Real World
Functional Programming

In Ads Serving

Sathish Kumar

Flipkart

Agenda

  • Flipkart and Ads
  • Imperative vs functional programming
  • Ads serving overview and Data Model
  • Ads Problem statements
    • Functional solutions
    • Java 8 vs Haskell
    • Abstractions, Monoids, Functors, Monads
    • Recurse

Real World

Real World

public List<Foo> getFooList(User user){
    return fooList.stream()
            .filter(createdBy(user))
            .filter(isActive())
            .orderBy(createdDate())
            .collect(toList());
}

Expectation

Reality

public List<Foo> getFooList(User user){
    List<Foo> fooList = new ArrayList<Foo>();
    List<Baz> bazList = BazStore.getAll();
    for (Baz baz : bazList) {
        if(baz.isActive()) {
            for (Bar bar : Bar.getAll(baz)) {
                if(bar.isCreatedByUser(user)){
                    for (Foo foo : Foo.getAll(bar)) {
                        fooList.add(foo);
                    }
                }
            }
        } else {
            fooList.add(Foo.getDefault(baz));
        }
    }
    return fooList;
}

Real World

class Book < ApplicationRecord
  validates :type, inclusion: { in: %w(paperback hardcover ebook)
  after_create :index_by_isbn
end

Expectation

Reality

book.save
nil
puts "#" * 100 ; p book
####################################################################################################
#<Book:0x007fb181077e40 @isbn="123456789", @id=nil>

Real World

Expectation

Reality

angular.module('user1', [])
.controller('UserController', function UserController() {
  this.username;
  this.email;

  this.login = function login(email, password) {
    return this.login(this.email, this.password, captcha());
  };
});

Fantasy World

of statically typed functional languages

qsort [] = []
qsort (x:xs) = qsort small ++ [x] ++ qsort large
  where small = [y | y <- xs, y <= x]
        large = [y | y <- xs, y > x]

QuickSort.hs

Fantasy World

of dynamically typed functional languages

(define (fibgen a b)
  (cons-stream a (fibgen b (+ a b))))

(define fibs (fibgen 0 1))

(take 10000 fibs)

FibonacciInfinite.scm

Fantasy meets Reality

Common Arguments for not using a Functional language:

  • Haskell / Lisp is not on the JVM
  • Scala / Clojure is hard to maintain
  • We need to build this in X months
  • It's easy to hire Java developers

Nobody understands
monads in Haskell

A language that doesn't affect the way you think about programming, is not worth knowing."

- Alan J. Perlis

Motivation

No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn't convenient" - John Carmack

Ads Serving

App

Desktop

Exchange

Ads Serving Stack

Ad Selection flow

Conventions

Functional concept introduced for the first time

# foo

foo :: String -> String
foo x = "Hello " ++ x
public String foo(String name){
    return "Hello " + name;
}

Haskell code example

Java code example

Data Model

Data Model

# Type constructors

# Type aliases

# Data constructors

using type and data constructors

type Id = Int
type Price = Double
data PriceType = CPM | CPV | CPC deriving (Eq, Show)

data Advertiser = Advertiser Id deriving (Eq, Show)
data Campaign = Campaign Id Advertiser PriceType Price deriving (Eq,Show)
data Banner = Banner Id Campaign deriving (Eq, Show)

data Slot = Slot Id
data SlotGroup = SlotGroup Id [Slot]
data User = UnknownUser | User Id 

type CTR = Double
type Score = Double
data BannerScore = BannerScore Banner Score

type ECPM = Double
@AllArgsConstructor
@EqualsAndHashCode
@ToString
class Advertiser {
    Integer id;
}












@AllArgsConstructor
@EqualsAndHashCode
@ToString
class Campaign {
    Integer id;
    Advertiser advertiser;
}

@AllArgsConstructor
@EqualsAndHashCode
@ToString
class Banner {
    Integer id;
    Campaign campaign;
}

Relevance - Campaigns

1. Active

2. Slot targeting

3. Gender targeting

Relevance - Campaigns

relevantCampaigns :: Context -> Map[Slot, [Campaign]]

1. Active

2. Slot targeting

3. Gender targeting

isSlotTargeted = \campaign, context ->
  getSlot campaign == getSlot context
isActive = \campaign -> getActive campaign
isGenderTargeted = \campaign, context ->
  case (getGender campaign) of
    Nothing -> True
    Just (gender) -> gender == getGender (getUser context)

# Type signature

# Lambdas

# Maybe / Optional

Relevance - Campaigns

Predicate<Campaign> isActive() {
    return campaign -> campaign.isActive();
}
Predicate<Campaign> isSlotTargeted(Context context) {
    return campaign -> campaign.getSlot().equals(context.getSlot());
}
Predicate<Campaign> isGenderTargeted(Context context) {
    return campaign -> context.getUser()
            .map(gender -> gender.equals(campaign.getGender()))
            .orElse(true);
}
List<Campaign> getRelevantCampaigns(Context context){
    return campaignStore.getAll().stream()
        .filter(isActive())
        .filter(isSlotTargeted(context))
        .filter(isGenderTargeted(context))
        .collect(toList());
}

Combining filters

# Streams

# Filter

Relevance - Campaigns

Predicate<Campaign> getFilters(Context context){
    return isActive()
            .and(isSlotTargeted(context))
            .and(isGenderTargeted(context));
}









List<Campaign> getRelevantCampaigns(Context context){
    return campaignStore.getAll().stream()
        .filter(getFilters(context))
        .collect(toList());
}

Combining predicates - AND

Adding a new targeting feature is now as easy as adding another predicate to this list

andAll :: [Campaign -> Bool] -> Campaign -> Bool
andAll predicates campaign = foldr (&&) True (map (\p -> p campaign) predicates)

# Folds / Reduce

Relevance - Banners

1. Matches template

(OR)

2. Matches width and height

Relevance - Banners

relevantBanners :: Map[Slot, [Campaign]] -> Map[Slot, [Banner]]

1. Matches template

(OR)

2. Matches width and height

forTemplate = \banner, context -> 
  getTemplate banner == getTemplate context
forDimension = \banner, context ->
  getWidth banner == getWidth context &&
    getHeight banner == getHeight context
Predicate<Banner> getFilters(Context context){
    return forTemplate(context)
            .or(forDimension(context));
}

List<Banner> getRelevantBanners(Context context, List<Campaign> campaigns){
    return bannerStore.getAll(campaigns).stream()
        .filter(getFilters(context))
        .collect(toList());
}

Relevance - Banners

Combining predicates - OR

orAll :: [a -> Bool] -> a -> Bool
orAll predicates x = foldr (||) False (map (\p -> p x) predicates)

Detour: Combining Bool functions

[1,2,3] ++ [] == [1,2,3]
[1,2] ++ ([3,4] ++ [5,6]) == ([1,2] ++ [3,4]) ++ [5,6]

[] ++ [1,2,3] == [1,2,3]

List

Sum

5 + 0 == 5
(1 + 2) + 3 == 1 + (2 + 3)

0 + 5 == 5

Product

5 * 1 == 5
(1 * 2) * 3 == 1 * (2 * 3)

1 * 5 == 5
False && True == False
(True && True) && False == True && (True && False)

True && False == False

Boolean AND

True || False == True
(True || True) || False == True || (True || False)

False || True == True

Boolean OR

What is the common pattern here?

All of these are binary operations which are associative with Identity

Monoid is an abstraction of this common pattern

Detour: Monoid

# Monoid

Any

mconcat [Any False, Any False] == Any False
mconcat [Any False, Any True] == Any True

All

mconcat [All True, All False] == All False
mconcat [All True, All True] == All True

Monoid: if you define identity and append, you get concat for free

class Monoid m where  
    mempty :: m  
    mappend :: m -> m -> m  
    mconcat :: [m] -> m  
    mconcat = foldr mappend mempty  
isRelevant campaign = mconcat (map (\f -> f campaign) filters)
isRelevant banner = mconcat (map (\f -> f banner) filters)

With these abstractions, Relevance simplifies to:

Ranking

1. Score banners by criteria

2. Sort banners by score

1. tryerlang.org (score: 0.85)

2. tryhaskell.org (score: 0.75)

3. tryclj.com (score: 0.65)

Ranking

rankBanners :: Map[Slot, [Banner]] -> Map[Slot, [BannerScore]] 

1. Score banners by criteria

2. Sort banners by score

Ranking: Generalizing Map transformations

What is the common pattern here?

relevantBanners :: Map[Slot, [Campaign]] -> Map[Slot, [Banner]]

rankBanners :: Map[Slot, [Banner]] -> Map[Slot, [BannerScore]] 
public static <K,A,B> Map<K, B> mapValues(Map<K, A> input, 
                                          Function<A, B> transformer) {
    return input.keySet().stream()
            .collect(toMap(Function.identity(), transformer));
}

# Higher order functions

Ranking

mapValues(relevantBanners, scorer())


public Map<Slot, List<BannerScore>> rankBanners(Map[Slot, List[Banner]] relevantBanners){
  return mapValues(relevantBanners, scorer());
}
Function<Slot, List<BannerScore>> scorer(){
    return slot -> {
        List<Price> prices = getPrices(banners);
        List<CTR> ctrs = getCTRs(banners);
        return StreamUtils.zip(
                    prices.stream(),
                    ctrs.stream(),
                    (price, ctr) -> new BannerScore(0.5 * price.getValue() + 0.5 * ctr.getValue()))
                .collect(toList());
    }
}

using first class functions

mapValues takes function as argument

scorer returns function as output

# First class functions

# zip / zipWith

Detour:

1. tryerlang.org (score: 0.90)

2. tryhaskell.org (score: 0.90)

3. tryclj.com (score: 0.90)

What if the scores are equal?

How can we rotate randomly?

compare(Banner b1, Banner b2){
    if(b1.score().compare(b2.score()) != 0){
        return b1.score().compare(b2.score())
    }
    return b1.random().compare(b2.random())
}
instance Monoid Ordering where  
    mempty = EQ  
    LT `mappend` _ = LT  
    EQ `mappend` y = y  
    GT `mappend` _ = GT  
score1 compare score2 <> 
  random1 compare random2

Ordering Monoid

Handling equal scores

CTR Prediction Model

1. Logistic Regression Model

2. Input: Supply, Demand, User features

3. Output: Predicted CTR

z=\theta_{0} + \theta_{1} * x_{1} + \theta_{2} * x_{2} ... + \theta_{n} * x_{n}
z=θ0+θ1x1+θ2x2...+θnxnz=\theta_{0} + \theta_{1} * x_{1} + \theta_{2} * x_{2} ... + \theta_{n} * x_{n}
pCTR = \frac{1}{1+e^{-z}}
pCTR=11+ezpCTR = \frac{1}{1+e^{-z}}

n features

θi - Model coefficients

Xi - Boolean value of features 

CTR Prediction Model

Features

1. Slot : slot:123 (0.001) or slot:default (-0.005)

2. Campaign : campaign:123 (0.002) or campaign:default (-0.005)

3. Gender : gender:male (0.003) or gender:female (0.003) or gender:default (-0.005)

4. Categories : category:books (0.004), category:mobiles (0.005), category:default (-0.005)

z = - 5.0 + 0.001 + 0.002 + 0.003 - 0.005

pCTR = 0.7%

CTR Prediction Model

# Optional map

# Optional flatMap

genderFeature :: Context -> Feature
genderFeature context = case (getGender (getUser context)) of
    Just(value) -> Feature "gender:" ++ value
    Nothing -> Feature "gender:default"
categoryFeature :: Context -> [Feature]
categoryFeature context = case (getCategories (getUser context)) of
    [] -> [Feature "cat:default"]
    values -> map (\value -> Feature "cat:" ++ value) values

# Optional orElse

# Null safety

Feature genderFeature = context.getUser().getGender()
    .map(value -> new Feature("gender:" + value))
    .orElse(new Feature("gender:default"))
Feature genderFeature = context.getUser()
    .flatMap(user -> user.getGender())
    .map(value -> new Feature("gender:" + value))
    .orElse(new Feature("gender:default"))

Single valued features

Multi valued features

Detour: Functors

req1 = Context {slot=1, user=Nothing}
req2 = Context {slot=1, user=Just User{gender="male"}}

generateFeature "gender" (gender (user req2)) -- won't work
generateFeature "gender" (fmap gender (user req1)) -- "gender:default"
generateFeature "gender" (fmap gender (user req2)) -- "gender:male"

generateFeature :: String -> Maybe String -> String
generateFeature fname Nothing = fname ++ ":default"
generateFeature fname (Just fvalue) = fname ++ ":" ++ fvalue

Functors apply a function to a value in a box

*

# Functors

instance Functor Maybe where  
    fmap f (Just x) = Just (f x)  
    fmap f Nothing = Nothing  

User::Maybe[User]

Gender::User -> String

Explore / Exploit

Multi-armed Bandit

*

N-armed bandit in a row of Slot machines

Arm - Options available

Reward - Success metrics

Exploitation - Choose highest reward

Exploration - Choose random reward

Explore/Exploit dilemma

 

Explore / Exploit

# map

if (shouldExplore()){
    return bannerExplorer(banners);
}
return bannerScorer(banners);
public Function<Slot, List<BannerScore>> bannerExplorer(List<Banner> banners){
    return slot -> banners.stream()
                    .map(b -> new BannerScore(b, random.nextDouble()))
                    .collect(toList());
}

Deduplication

1. Serve unique ads across multiple slots

2. Uniqueness can be by advertiser or by campaign

 

Deduplication

Map<Advertiser, List<Banner>> bannersByAdvertiser = banners.values()
    .stream()
    .collect(groupingBy(b -> b.getCampaign().getAdvertiser()));


boolean isDedupRequired(Map<Slot, Banner> banners){
    Map<Advertiser, List<Banner>> bannersByAdvertiser = banners.values()
        .stream()
        .collect(groupingBy(b -> b.getCampaign().getAdvertiser()));
    return bannersByAdvertiser.size() != banners.size();
}

# groupBy

dedupBanners :: Map[Slot, [BannerScore]] -> Map[Slot, Maybe[Banner]] 

count(advertisers) != count(banners)

Detour : Applicatives

Dedup options:

1. None 2. By Advertiser 3. By Campaign

dedupCriteria = Nothing
dedupCriteria = Just (\banner -> (campaign banner))
dedupCriteria = Just (\banner -> (advertiser (campaign banner)))

# Applicative

instance Applicative Maybe where  
    Nothing <*> _ = Nothing  
    (Just f) <*> something = fmap f something  

Applicatives apply function in a box

to a value in a box.

*

Attribution

A/B test to find effectiveness of Banner

A B
1% 1.5%

Attribution

splitBanners :: Map[Slot, [Banner]] -> Map[Slot, [Banner]] 
allBanners = banners.values().stream()
        .flatMap(bs -> bs.stream())
        .collect(toList());

abService.getControl(context.getUser(), allBanners)
abService.getTreatment(context.getUser(), allBanners)

# flatMap

Detour : 

Feature genderFeature = context.getUser()
    .flatMap(user -> user.getGender())
    .map(value -> new Feature("gender:" + value))
    .orElse(new Feature("gender:default"))

flatMap on Optional

Gender feature

banners.values().stream()
    .flatMap(bs -> bs.stream())
    .filter(banner -> abService.isA(user, banner))
    .collect(toList());

Attribution

flatMap on Stream

What is the common pattern here?

What if we use map instead of flatMap here?

context = Context {slot=1, user=Just User{gender=Just "male"}}
fmap gender (user context)
-- Just (Just "male")
(user context) >>= gender
-- Just "male"

user :: Context -> Maybe User
gender :: User -> Maybe String
-- fmap (User -> Maybe String) (Maybe User) :: Maybe (Maybe User)

Detour : 

# Monads

Maybe Monad

Gender feature

Monad is an abstraction of this common pattern

(>>=) :: Monad m => ma -> (a -> mb) -> mb 
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
(>>=) :: Maybe User -> (User -> Maybe Gender) -> Maybe Gender

*

Monads apply a function that returns a value in a box to a value in a box

user::Context -> Maybe[User]

gender::User -> Maybe[String]

Workflow

# Function composition

Workflow is nothing but function composition

compose :: (b -> c) -> (a -> b) -> a -> c
relevantCampaigns :: Context -> [Campaign]
relevantBanners :: [Campaign] -> [Banner]
splitBanners :: [Banner] -> [Banner]
rankBanners :: [Banner] -> [BannerScore]
dedupBanners :: [BannerScore] -> Maybe[Banner]
selectAds :: Context -> Maybe[Banner]
selectAds = dedupBanners . rankBanners . splitBanners . relevantBanners . relevantCampaigns

Questions?

@sathish316

Sathish Kumar

Flipkart

  • Pros
    • Immutable code
    • What vs How
    • Null safety
    • Refactoring 
    • Parallel streams and Multicore
    • Read heavy
    • Caching
    • System performance
    • Shoulders of giants
  • Cons
    • Impedance mismatch - Streams and Collect
    • Overuse of Streams
    • Mixing mutable state
    • No Currying, Pattern matching
    • Write heavy?
    • Performance of streams vs loops
Made with Slides.com