Nice builder pattern

Anybody written a builder pattern like this? For cases where you have quite large records with lots of fields that are optional, and frequently left at their default values.

Its nice to use in the sense that you only need to set things when they are actually needed. Also if you have several records like this in a more complex data model and the same field names exist in multiple record types, by scoping the setter functions inside the builder you avoid the name clashes you would get if they were top-level functions.

example : LongOptionalRecord
example =
    builder "id-123" "Widget"
        .addParam "p1"
        .addParam "p2"
        .withAttr "env" "prod"
        .withOption "special"
        .addAndSoOn 42
        .addAndSoOn 7
        .build

type alias LongOptionalRecord =
    { name : String -- required
    , id : String -- required
    , params : List String
    , attrs : Dict String String
    , option : Maybe String
    , andSoOn : Set Int
    }

type Builder =
    Builder
      { withParams : List String -> Builder
      , addParam : String -> Builder
      , withAttrs : Dict String String -> Builder
      , withAttr : String -> String -> Builder
      , withOption : String -> Builder
      , withoutOption : Builder
      , withAndSoOn : Set Int -> Builder
      , addAndSoOn : Int -> Builder
      , build : LongOptionalRecord
      }


builder : String -> String -> Builder
builder id name =
    builderFrom
        { name = name
        , id = id
        , params = []
        , attrs = Dict.empty
        , option = Nothing
        , andSoOn = Set.empty
        }


builderFrom : LongOptionalRecord -> Builder
builderFrom rec =
    { withParams =
        \params ->
            builderFrom { rec | params = params }
    , addParam =
        \param ->
            builderFrom { rec | params = rec.params ++ [ param ] }
    , withAttrs =
        \attrs ->
            builderFrom { rec | attrs = attrs }
    , withAttr =
        \key value ->
            builderFrom { rec | attrs = Dict.insert key value rec.attrs }
    , withOption =
        \value ->
            builderFrom { rec | option = Just value }
    , withoutOption =
            builderFrom { rec | option = Nothing }
    , withAndSoOn =
        \ints ->
            builderFrom { rec | andSoOn = ints }
    , addAndSoOn =
        \n ->
            builderFrom { rec | andSoOn = Set.insert n rec.andSoOn }
    , build = rec
    }
    |> Builder
6 Likes

Hey Rupert,

Many thanks for sharing this design. I was not aware of this pattern.
This is going to improve readability in several places I have been working on.

Would be very cool and clean, but doesn’t seem to work because Builder cant be accessed by field since it is not a record, but a type constructor with a record.
Also .addParam in this case is a getter, not a field accessor. So the syntax would not work this way aswell.

Yeah, I see that now. It was originally just a record, but then I had to make it a type since it is recursive, so simple field accessors do not work.

I will iterate a bit and see if it can be worked out.

Best I could come up with is this:

exampleRecord : RecordBuilder.Record
exampleRecord =
    recordBuilder "id-123" "Example"
        |> andRecord .withParams [ "p1", "p2" ]
        |> andRecord .addParam "p3"
        |> andRecord .withAttr "color" "blue"
        |> andRecord .withAttr "version" "1.0"
        |> andRecord .withOption "enabled"
        |> andRecord .addAndSoOn 42
        |> andRecord .addAndSoOn 99
        |> .build

And builder code:

type alias Record =
    { name : String
    , id : String
    , params : List String
    , attrs : Dict String String
    , option : Maybe String
    , andSoOn : Set Int
    }


type alias RecordBuilder =
    { builder : Inner
    , build : Record
    }


type Inner
    = Wrapper RecordCons


andRecord :
    (RecordCons -> a -> RecordBuilder)
    -> a
    -> RecordBuilder
    -> RecordBuilder
andRecord fld val b =
    let
        (Wrapper funs) =
            b.builder
    in
    fld funs val


type alias RecordCons =
    { withParams : List String -> RecordBuilder
    , addParam : String -> RecordBuilder
    , withAttrs : Dict String String -> RecordBuilder
    , withAttr : String -> String -> RecordBuilder
    , withOption : String -> RecordBuilder
    , withoutOption : RecordBuilder
    , withAndSoOn : Set Int -> RecordBuilder
    , addAndSoOn : Int -> RecordBuilder
    }


recordBuilder : String -> String -> RecordBuilder
recordBuilder id name =
    let
        mk : Record -> RecordBuilder
        mk rec =
            { builder =
                { withParams =
                    \params ->
                        mk { rec | params = params }
                , addParam =
                    \param ->
                        mk { rec | params = rec.params ++ [ param ] }
                , withAttrs =
                    \attrs ->
                        mk { rec | attrs = attrs }
                , withAttr =
                    \key value ->
                        mk { rec | attrs = Dict.insert key value rec.attrs }
                , withOption =
                    \value ->
                        mk { rec | option = Just value }
                , withoutOption =
                        mk { rec | option = Nothing }
                , withAndSoOn =
                    \ints ->
                        mk { rec | andSoOn = ints }
                , addAndSoOn =
                    \n ->
                        mk { rec | andSoOn = Set.insert n rec.andSoOn }
                }
                    |> Wrapper
            , build = rec
            }
    in
    { name = name
    , id = id
    , params = []
    , attrs = Dict.empty
    , option = Nothing
    , andSoOn = Set.empty
    }
        |> mk

Another way that is probably neater, would be to bundle a standard builder pattern as record-of-functions.

I suspect the recursive one might be quite innefficient too, as creating a new record of lambdas, only one of which will be used, at each step.

exampleRecord : RecordBuilder.Record
exampleRecord =
    let
        b =
            recordBuilder
    in
    b.record "Example" "id-123"
        |> b.withParams [ "p1", "p2" ]
        |> b.addParam "p3"
        |> b.withAttr "color" "blue"
        |> b.withAttr "version" "1.0"
        |> b.withOption (Just "enabled")
        |> b.addAndSoOn 42
        |> b.addAndSoOn 99

Builder implementation:

type alias Record =
    { name : String
    , id : String
    , params : List String
    , attrs : Dict String String
    , option : Maybe String
    , andSoOn : Set Int
    }


type alias RecordBuilder =
    { record : String -> String -> Record
    , withParams : List String -> Record -> Record
    , withAttrs : Dict String String -> Record -> Record
    , withOption : Maybe String -> Record -> Record
    , withAndSoOn : Set Int -> Record -> Record
    , addParam : String -> Record -> Record
    , withAttr : String -> String -> Record -> Record
    , addAndSoOn : Int -> Record -> Record
    }


recordBuilder : RecordBuilder
recordBuilder =
    { record =
        \name id ->
            { name = name
            , id = id
            , params = []
            , attrs = Dict.empty
            , option = Nothing
            , andSoOn = Set.empty
            }
    , withParams =
        \params rec ->
            { rec | params = params }
    , withAttrs =
        \attrs rec ->
            { rec | attrs = attrs }
    , withOption =
        \mOption rec ->
            { rec | option = mOption }
    , withAndSoOn =
        \ints rec ->
            { rec | andSoOn = ints }
    , addParam =
        \param rec ->
            { rec | params = rec.params ++ [ param ] }
    , withAttr =
        \key val rec ->
            { rec | attrs = Dict.insert key val rec.attrs }
    , addAndSoOn =
        \n rec ->
            { rec | andSoOn = Set.insert n rec.andSoOn }
    }
2 Likes

This looks pretty close to the elm-serialize code for records and custom types

2 Likes

I have a feeling I must be really missing something but if you’re really just looking for

For cases where you have quite large records with lots of fields that are optional, and frequently left at their default values.

it feels like you’re over thinking it to me.

If I would be in said situation I would not even try to invent any abstraction and just use what language gives you (language itself, not even core library).

type alias Person =
    { name : String
    , age : Int
    , unlikely : Bool
    }


defaultPerson : Person
defaultPerson =
    { name = "Jane Doe"
    , age = 0
    , unlikely = False
    }


people : List Person
people =
    [ { defaultPerson | age = 42 }
    , { defaultPerson | name = "James" }
    , { defaultPerson | name = "Creep", unlikely = True }
    ]

If you’re looking for something more advanced, and I really can imagine it only in more interesting cases like where stuff could fail or so, then I would create something with applicative or even a pro-functor interface. This is common in things like Validators. I have an extensive validator library somewhere (not published) I can copy snippets from to give example if that would clarify what I mean.

Anyway I wonder what I’m missing and why this simple way of doing this won’t do the trick for you?

〈ot-rant〉I don’t know what programmers of discourse are inhaling with the stupid behaviour of the editor but someone should stop them from getting near computers because only thing they bring is pain〈/ot-rant〉

2 Likes

Yeah, the issue I am trying to solve is this:

type alias LongRecord = 
    { id : String
    , name : String
    , attribute, Dict String String
    ... -- lots more fields
    }

type alias AnotherLongRecord = 
    { id : String
    , name : String
    , attribute, Dict String String
    ... -- lots more fields with overlapping names to LongRecord
    }

type alias AndSoOn = ...

So two things really.

  1. I have many longish records where most of the time, most of the field values will just take their default values (empty List, empty Dict, and so on).
  2. In 1 module I have multiple such records, and they tend to have a lot of overlap in the field names.

Then I have code that builds lots of these records, so I definitely do not want to repeat the setting of defaults over and over - hence a builder pattern is quite efficient.

But I do not want to have to fully qualify all the builder functions by record type like anotherLongRecordWithAttributes because its just too much typing really. And then the module exports list will be massive too.

Some other ideas I have are:

  • Add separate builder modules, 1 per record type, then the builder functions are fully qualified by module.
  • Use polymorphic builder functions:
withAttributes : a -> { x | attributes : a } -> { x | attributes : a } 

But.. its nice to have type sigs with concrete field types, and maybe this isn’t so helpful for situation such as when I want “smart” constructors that might fail in some situations and not others and so on.

Ultimately looking for builder patterns that will let me write fairly convenient and compact code, since I will be using them a lot.

They messed up the editor, but you can switch back to the old one in the editing menu.

1 Like

I’m sorry, I don’t really understand what you’re trying to achieve here.

In 1 module I have multiple such records, and they tend to have a lot of overlap in the field names.

But why are they in the same module then?

Add separate builder modules, 1 per record type, then the builder functions are fully qualified by module.

This seems like the obvious solution that would “just work”.

You seem concerned that the types are opaque? Why? I think because you want to ensure that records are created with the correct default values (and I suppose that you could change those default values). If you make each type opaque in their own dedicated module (or even in one big module I guess) the downside is that you necessarily have to write auxiliary getter/setter functions for each field (they can of course be easily generated).

Personally, I’d take a step back, and really ask myself whether these types need to be opaque, what errors would that prevent?

One other point, it sounds like you maybe have quite a lot of large records. Do they reference each other? You may run into the compiler issue that large record types cause long compile times (because the .elmi files in elm-stuff become large (and then take a long time to parse) because each record type is fully (and recursively) expanded. In which case, making those types opaque does fix that issue. You can use the following script to see if any of your .elmi files are getting large:

#!/bin/sh

du -hs elm-stuff/0.19.1/* | sort -h | tail -n 50
du -hs elm-stuff/0.19.1/

This will show you 50 largest .elmi/.elmo files in order, and also how much space the whole directory is taking up.

p.s. I also cannot stand the new editor, but I don’t seem to have any option in the editing menu to switch back to the old one :frowning:

It is convenient to have the data model and a builder API all in a single module. I tend to favour importing just 1 module, over importing many modules for things like this.

They are all part of a single data model - think tree with many different node and leaf types. It is an AST for codegen but the nodes and leafs tend to have quite a few common fields. I suppose another approach might be to pull up the common fields into a base record:

type alias Base a = 
    { a |
      id : String
    , name : String
    , attribute, Dict String String
    }

withAttributes : Dict String String -> Base a -> Base a

No, they are not opaque types, just records.

Not that big.

1 Like

I probably still don’t fully get it. Which I think is expected - The small snippet doesn’t tell the whole story and this seems to be an pain point caused by scale.

If I at least partially get it then I can at least say what I would do.

I would define defaults for all these records as such:

type alias LongRecord = 
    { id : String
    , name : String
    , attribute, Dict String String
    -- ... lots more fields
    }

longRecord : LongRecord
longRecord =
      { id = ""
    , name = ""
    , attribute = Dic.empty
    -- ... lots more fields
     }

type alias AnotherLongRecord = 
    { id : String
    , name : String
    , attribute, Dict String String
    -- ... lots more fields with overlapping names to LongRecord
    }

anotherLongRecord : AnotherLongRecord
anotherLongRecord =
     { id = ""
    , name = ""
    , attribute = Dict.empty
    -- ... lots more fields with overlapping names to LongRecord
    }

And then use row polymorphism (extensible records) for shared setters

setId : String -> { a | id : String } -> { a | id : String }
setId id rec =
   { rec | id = id }

setName : String -> { a | name : String } -> { a | name : String }
setName name rec =
   { rec | name = name }

This you can “compose” to sort of factory pattern without using any novel abstraction like:

a = longRecord |> setId "foo" |> setName "Ken Thompson"
b = anotherLongRecord |> setId "bar" |> setName "Dennis Ritchie"

That at least to me seems like a sufficient solution. We can call it “first class factory pattern” since it’s using only first-class language features.

I think doing something more fancy would involve a lot of trade-offs in elm because it doesn’t give you ad-hoc polymorphism which would let you wire some implicit arguments/contexts around and it doesn’t come with more extensive row type support like ability to extend record with new field such as:

addFieldToRec : String -> { a } -> { a | newFiled : String }
addFieldToRec str rec =
   { rec | newField = str }

Or even more generic ways to turn record fields (rows) to list and create records from lists of pairs or so.

1 Like

I’ve been very satisfied using the same pattern that elm’s Html uses, passing a list of attributes (or parameters) while initializing a type or record.

The type Attribute can be an opaque type or just a function from record to record, I prefer an opaque type.

It can also be parameterizable for extensible records or parameterized types Attribute msg


{-| Represents an attribute that can be applied to a LongOptionalRecord.
-}
type Attribute a
    = Attribute (LongOptionalRecord a -> LongOptionalRecord a)


type alias LongOptionalRecord a =
    { name : String
    , id : String
    , strParams : List String
    , attrs : Dict String String
    , option : Maybe a
    , soOns : Set Int
    }


{-| Initialize a LongOptionalRecord with required fields and a list of attributes.
-}
init : String -> String -> List (Attribute a) -> LongOptionalRecord a
init id name attributes =
    List.foldl (\(Attribute f) rec -> f rec)
        { name = name
        , id = id
        , strParams = []
        , attrs = Dict.empty
        , option = Nothing
        , soOns = Set.empty
        }
        attributes


-- Attribute constructors

strParam : String -> Attribute a
strParam param =
    Attribute (\rec -> { rec | strParams = param :: rec.strParams })


attr : String -> String -> Attribute a
attr key value =
    Attribute (\rec -> { rec | attrs = Dict.insert key value rec.attrs })


soOn : Int -> Attribute a
soOn n =
    Attribute (\rec -> { rec | soOns = Set.insert n rec.soOns })


option : a -> Attribute a
option a =
    Attribute (\rec -> { rec | option = Just a })


-- Usage example

type Stuff
    = Stuff


example : LongOptionalRecord Stuff
example =
    init "id-123" "Widget"
        [ strParam "p1"
        , strParam "p2"
        , attr "env" "prod"
        , option Stuff
        , soOn 42
        , soOn 7
        ]


1 Like

We had some code that is using this in our largest codebase in the company. I personally hate it and am slowly rewriting away from it. But in our case it’s mostly to raw Html.Attribute and use Decoders and stuff to remove the need to even have the records in these APIs.

It maybe provides interface similar to Html package which almost everyone likes but it’s not the same. This record backed interface makes it hard to compose more things together. It makes the API overly closed. In Html API is very open with functions like Html.Attributes.attribute : String -> String -> Html.Attribute msg. You don’t need to go and extend some internal record to define new attribute. In fact individual prorties are defined as:

class : String -> Attribute msg
class =
  stringProperty "className"

in the source of Html package.

It might be a matter of taste but at least in our codebase where this is esentially used on some view functions to create distinct attribute types only those view support this makes it hard to use other Html attributes one might need and it creates a lot of uninteresting boilerplate which does nothing but plumbing. And to me it feels like the primary edge one has using functional language is an ability to compose many dumb things together easily rather than building elaborate ways to initialize values.

This doesn’t mean you’re wrong doing it. You’re free to code in any style you like. But I think even my perspective is valid in some way so I wanted to share it in case someone finds it helpful or interesting.

I agree, I used the constructor pattern as well, where it make sense. I don’t have an specific example where I prefer one or the other but I end up using both of them.

1 Like

I can imagine this could feel much more right in library which provides comprehensive functionality. But I would say one should be carful building their app like this because application code grows very unevenly and in unexpected ways and it’s much harder to have comprehensive picture about things up front.

I believe is well suited for APIs where you’re building a tree with many nested instances, and that is why it works and maps well to Html, at least that’s where I prefer the list of attributes to the builder pattern.

When ever I’m building some sort of a tree I always separate the structure from the content and use some sort of polymorphic Tree a/Layout a and then fill in what ever the a should be separately. I tend to do that even if I need just a single instance of it even though more often than not there are more things to fill the a with. Just the existance of any a forces the separation of manipulation of structure from the specifics of the data. It reminds me also of some points from this talk which packs a lot of insight https://www.youtube.com/watch?v=hIZxTQP1ifo

Another option is to stick each of the functions you want to qualify in a record. They can stay in the same module, but will be “namespaced”.