Post Examples of Painful Record Updates Here!

1,208 views
Skip to first unread message

Richard Feldman

unread,
Mar 3, 2017, 1:12:39 AM3/3/17
to Elm Discuss
There have been various discussions of potential ways to improve Elm's record update syntax. Evan commented that "(examples > design work) at this point" - any potential designs for syntax improvements would need to be run through a gauntlet of examples to see how well they'd work, so the first step in the process is to gather those examples.

So let's collect a ton of different real-world examples! That will help guide the design process.

If you've run into a record update that you felt was painful and could be improved in some way, please post it here! (Also, please keep this thread for posting of examples only - it'll be easier to link back here during design discussions if we can reference a clean thread of examples, as opposed to a mismash of examples interleaved with suggestions.)

Richard Feldman

unread,
Mar 3, 2017, 1:16:15 AM3/3/17
to Elm Discuss
Re-posting the first example from Franscisco's thread:

There is a common pattern where a library (ex, elm-markdown) will provide a default config, to be extended by the user.

Here the two ways to do this right now, one painfully verbose and the other relying on exposing values that would read better if fully qualified instead of exposed:

import Slides

slidesDefaultConfig 
=
  
Slides.defaultConfig

myCustomSlidesConfig 
=
 
{ slidesDefaultConfig | someAttribute = myCustomvalue }

or

import Slides exposing (slidesDefaultConfig)

myCustomSlidesConfig 
=
 
{ slidesDefaultConfig | someAttribute = myCustomvalue }

Not a big deal TBH, but annoying.


Rupert Smith

unread,
Mar 3, 2017, 7:15:59 AM3/3/17
to Elm Discuss
On Friday, March 3, 2017 at 6:16:15 AM UTC, Richard Feldman wrote:
Re-posting the first example from Franscisco's thread:

I think item 2 from Franscisco's thread is looking for an easy way to add and remove fields from a record, producing a new record with a different type. Let me just abuse the list append (++) operator to give the idea of how a field would be added:

someRecord ++ { newField = "blah" }

The long hand version might be

addNewField someRecord = 
  { newField = "blah", someRecord.existingField1, someRecord.existingField2, ... }

Not sure how I feel about operations that produce records of a different type, but if its really just syntax sugar for something you can write out in long hand, it does not seem so unreasonable.

Rupert Smith

unread,
Mar 3, 2017, 7:18:53 AM3/3/17
to Elm Discuss
Another one I have run into, is when selecting just one field from a record, there is no syntax to further pattern match on it. Again, abusing the 'as' syntax in a quite different way to how it is currently used, something along these lines:

func { field } =
  let
     SomeConstructor arg = field
  in
     arg

Could be shortened to:

func { field as SomeConstructor arg } = arg

Sorry, that wasn't an update but it relates to record syntax.

Richard Feldman

unread,
Mar 3, 2017, 8:46:36 AM3/3/17
to Elm Discuss

Can we express these in terms of examples of real-world code that is currently painful? That's really the key here. :)


--
You received this message because you are subscribed to a topic in the Google Groups "Elm Discuss" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elm-discuss/oWfARte8DJU/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Matthew Griffith

unread,
Mar 3, 2017, 9:29:27 AM3/3/17
to Elm Discuss



Here's an example from elm-style-animation, looking at animating a list of something like cards.  

type alias Model =
   
{ cards : List Card }

type
alias Card =
   
{ content : String
   
, imageSrc : Msg
   
, likes : Int
   
, style : Animation.State
   
}



... basic update.  Doesn't look too bad.

   Animate time ->
        let
            updateCard card
=
               
{ card | style = Animation.update time card.style }
       
in
           
( { model
               
| cards =
                   
List.map updateCard model.cards
             
}
           
, Cmd.none
           
)




... update with Animation.Messenger, which returns (Animation.State, Cmd Msg)

 
  Animate time ->
        let
            updateCard card
(updatedCards,cmd) =
                let
                   
(newStyle, newCmd) = Animation.Messenger.update time card.style

                    newCard
= { card | style = newStyle }
               
in
                   
( updatedCards ++ [ newCard ]
                   
, Cmd.batch [ newCmd, cmd ]
                   
)


           
(updatedCards, cmds) =
               
List.foldl
                    updateCard
                   
([], Cmd.none)
                    model
.cards
       
in
           
( { model | cards = updatedCards }
           
, cmds
           
)



The `Animate` msg can get pretty hairy when you have a bunch of different animations to keep track of.

Brian Hicks

unread,
Mar 3, 2017, 9:43:58 AM3/3/17
to Elm Discuss
Noah posted an alternate syntax for JSON decoders that could make use of a better record update syntax. (post: https://medium.com/@eeue56/json-decoding-in-elm-is-still-difficult-cad2d1fb39ae#.ia6brcxw5)

In short, he suggests decoupling the decoder order from the record order using the following API:

decodeModel : Json.Value -> Result String Model
decodeModel value
=
 
Ok { name = "", age = 0 }
   
|> decodeField "age" Json.int setAge value
   
|> decodeField "name" Json.string setName value

Where setName and setAge have to be defined as such:

setName : Model -> String -> Model
setName model name
=
   
{ model | name = name }


Brian Hicks

unread,
Mar 3, 2017, 10:14:34 AM3/3/17
to Elm Discuss
The Mantl UI combines several different kinds of data across a large platform-as-a-service-like system. At the top level, I'm handling messages for the model/view/update triples and handling routing. This may not be the best architecture, but it's what I came up with as a beginner and I can't think of ways to improve it substantially (not that I would anyway… it compiles and runs fine and the contract is over.)

The painful bit is at https://github.com/CiscoCloud/mantl-ui-frontend/blob/master/app/Mantl.elm#L61-L90. To get everything in one place, here's the code:

update : Msg -> Model -> ( Model, Cmd Msg )
update action model =
    case action of
        Refresh ->
            model
                ! [ Cmd.map VersionMsg Version.loadVersion
                  , Cmd.map HealthMsg Health.loadHealth
                  , Cmd.map ServicesMsg Services.loadServices
                  ]

        ServicesMsg sub ->
            let
                ( services, cmd ) =
                    Services.update sub model.services
            in
                { model | services = services } ! [ Cmd.map ServicesMsg cmd ]

        VersionMsg sub ->
            let
                ( version, cmd ) =
                    Version.update sub model.version
            in
                { model | version = version } ! [ Cmd.map VersionMsg cmd ]

        HealthMsg sub ->
            let
                ( health, cmd ) =
                    Health.update sub model.health
            in
                { model | health = health } ! [ Cmd.map HealthMsg cmd ]

I had tried to make this more generic, and was mostly able to, but couldn't because I couldn't figure out record update syntax (spoiler alert: I was a newbie and there wasn't one but I even went and looked at the compiler source to figure it out.) Of course now I would just make a setter function, but the more you know 🌈

Rupert Smith

unread,
Mar 3, 2017, 11:14:38 AM3/3/17
to Elm Discuss
I wanted to be able to add fields to records, but when I found out I could not, then I changed the way I create data models; which is to simply nest smaller records inside in such a way that each partitions off a piece of the data that behaves as an independent unit. As per the recommendation that nesting is better than extending: https://groups.google.com/forum/#!msg/elm-discuss/AaL8iLjhEdU/pBe29vQdCgAJ

So I do not need this syntax any longer myself, since I have a way to break down bigger models into piece and put pieces back together again to form bigger or transformed models. In fact I think not having it has made me consider my data models more carefully and to build better ones. Being able to add and remove fields feels more like javascript where you don't declare the type of things up-front, you just keep hacking more fields on.

Mark Hamburg

unread,
Mar 3, 2017, 11:45:01 AM3/3/17
to elm-d...@googlegroups.com
Our codebase suffers from this as well. And unlike what one of the follow ups noted, this isn't an issue of wanting to add fields. Rather, it's an issue of not being able to use an expression in the first part of the record update. In this case, one doesn't even need a general expression but just an imported value. On the  their hand, members of my team have gotten bit by and complained about not being able to construct a nested update using what they thought were the language constructs:

    newFoo = { oldFoo | nested = { oldFoo.nested | field = 3 } }

The "best" syntax I've seen for that is something like:

    newFoo =
         3 |> asFieldIn oldFoo.nested |> asNestedIn oldFoo

But that's pretty obscure as well.

That said, I just added a utility module to our project that is filled with generic setFoo and asFooIn functions for every field name that seems at all likely to be used more than once. I would love to have support for .field= functions to parallel the .field functions. I would have them take the assigned value as a first parameter and the record as a second parameter to enable writing:

    Slides.defaultConfig
        |> .someAttribute= myCustomValue

The case where the parameters are reversed which helps with nesting could be handled using a utility function:

     3
        |> into oldFoo.nested .field=
        |> into oldFoo .nested=

Mark
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

Murphy Randle

unread,
Mar 3, 2017, 12:14:58 PM3/3/17
to Elm Discuss
Here's an example of where record field pattern matching would make things more straight forward:

case model.route of
   Routes.Entry { mode } ->
       if mode == Read then
           (hex "FFFFFF")
       else
           (hex "f6f6f6")

    _ ->
       (hex "f6f6f6")

If we were able to pattern match on values within records, it could look like this instead:

case model.route of
    Routes.Entry { mode = Read } -> hex "ffffff"
    _ -> hex "f6f6f6"

I think that's much more readable.

Without pattern matching, I had to repeat the value in the default case twice, potentially leading to subtle bugs. I could do something like this to avoid that:

let
  default = hex "f6f6f6"
in
  case model.route of
      Routes.Entry { mode } ->
          if mode == Read then
              (hex "FFFFFF")
          else
              default

      _ ->
        default


But I think that's even less readable.

Richard Feldman

unread,
Mar 3, 2017, 1:03:36 PM3/3/17
to Elm Discuss
On Friday, March 3, 2017 at 8:45:01 AM UTC-8, Mark Hamburg wrote:
Our codebase suffers from this as well.

Mark, can you post some examples of painful code? As in, not just what you wanted to write but couldn't, but what you ended up writing that's still painful.

Real code is much more useful than hypothetical examples that use foo! :)

Antoine Snyers

unread,
Mar 3, 2017, 2:49:50 PM3/3/17
to Elm Discuss
What I find painful is when I need to check for cases inside cases in order to decide whether something should be set or should be kept.
( { model | mode =
       
case model.mode of
           
Drawing maybe ->
               
case maybe of
                   
Just shape ->
                       
Drawing <| Just <| Shapes.setID id shape
                    _
->
                        model
.mode
            _
->
                model
.mode
 
}
, Cmd.none
)

Matthieu Pizenberg

unread,
Mar 4, 2017, 12:00:13 AM3/4/17
to Elm Discuss
Hi Antoine, just to let you know that you could write:
( { model | mode =
       
case model.
mode of
           
Drawing (Just shape) ->

               
Drawing <| Just <| Shapes.setID id shape

            _
->
                model
.mode
 
}
, Cmd.none
)

Nested pattern matching is allowed for many constructions. Though I usually prefer having a clear return value, and define intermediate values like:

let
    newMode
=
       
case model.mode of
           
Drawing (Just shape) ->

               
Drawing <| Just <| Shapes.setID id shape

            _
->
                model
.mode
in
   
( { model | mode = newMode }
   
, Cmd.none
   
)

Which is still verbose but because of code logic, not record syntax IMHO.

Francesco Orsenigo

unread,
Mar 4, 2017, 4:45:12 PM3/4/17
to Elm Discuss
Thank you Richard for rescuing my confused attempt. ^_^

I'd like to add another example, hopefully better explained:
Given:

type alias RuleContentAnd a =
   
{ a
       
| category : Category
       
, name : String
       
, template : Template
       
, view : View
       
, arguments : List Argument
   
}

type
alias RuleContent =
   
RuleContentAnd {}

type
alias Rule =
   
RuleContentAnd { id : Id }


Converting from one to the other is pure boilerplate:

ruleToRuleContent : Rule -> RuleContent
ruleToRuleContent rule
=
   
{ arguments = rule.arguments
   
, name = rule.name
   
, template = rule.template
   
, view = rule.view
   
, category = rule.category
   
}


I would like to be able to write something like

ruleToRuleContent : Rule -> RuleContent
ruleToRuleContent rule
=
  removeAttribute
.id rule


Nate Abele

unread,
Mar 4, 2017, 8:17:56 PM3/4/17
to Elm Discuss
On Friday, March 3, 2017 at 1:12:39 AM UTC-5, Richard Feldman wrote:
Hi all, Richard asked me to post to this thread based on a discussion we had. Following along with some running themes here, I'm working with nested Maybes, and combinations of Maybes with union types, which can quickly lead to deeply nested patterns. Current code:

moveLeft : ExpressionTree -> ExpressionTree
moveLeft content =
    case (activeBuffer content) of
        Just buffer ->
            case buffer of
                BufferContent buffer ->
                    case buffer.cursor of
                        Just cursor ->
                            if Cursor.atStart buffer cursor then
                                content -- @TODO Shift last prev onto current
                            else
                                -- ...
                        Nothing ->
                            content
                _ ->
                    content
        Nothing ->
            content

Theoretical future code:

moveLeft content =
    case activeBuffer content of
        Just (BufferContent { cursor = Just cursor }) ->
            if atStart buffer cursor then
                content
                -- @TODO Shift last prev onto current
            else
                -- ...

        _ ->
            content

Being able to destructure within a pattern match would save some extra boilerplate.

Martin Bailey

unread,
Mar 5, 2017, 1:56:42 PM3/5/17
to Elm Discuss
Here are some real world examples of record updates that could be streamlined at the language level, including attempts to simplify UI code defining record updates.

-- Update.elm
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Update updater ->
            ( updater model, Cmd.none )
        SaveFamily family ->
            ( model, Cmd.saveFamily family )
        _ ->
            ( model, Cmd.none )

-- View.elm
selected_family : Model -> UI.Node Msg
selected_family model =
    UI.form
        [ UI.class "fluid"
        , UI.onSubmit <| SaveFamily model.family
        ]
        [ UI.input
            [ UI.label "Name"
            , UI.value model.family.name
            , UI.onInput <| \val -> Update <| familyUpdater (\family -> { family | name = val })
            ]
        ]

familyUpdater : (Family -> Family) -> Model -> Model
familyUpdater updater model =
    { model | family = updater model.family }

-- familyUpdater looks simple enough, but at production scale with multiple embedded records, a few of these functions must be chained together to complete what could be a simple update such as model.family.contact.address.street = "Main Street"
-- Multiple specific Msg types such as UpdateFamily can be created to "clean" the view, but then it becomes really hard to achieve separation of concern as the Update function grows with out-of-context logic. Those updater functions are then moved to the Update file or you have to use many layers of let bindings. There are two concerns here, the update behavior being defined in the context of the form or becoming overly complex due to embedded records. The latter is more important.
-- Here's an example from another real world app :

    RcvGeocode geocode ->
        let
            address =
                extractAddress geocode

            updated =
                address |> updateAddress |> updateContact |> updateProfile |> updateUser model
        in
            ( { updated | geocode = geocode }, Cmd.none )

-- This would look much nicer in my opinion :

    RcvGeocode geocode ->
        ( { model | user.profile.contact.address = extractAddress geocode }, Cmd.none )

Francesco Orsenigo

unread,
Mar 5, 2017, 4:09:08 PM3/5/17
to Elm Discuss

Hi Martin, I wonder if for your case a different way to organise your code would be viable:

-- Family.elm
updateFamily msg family
=
 
case msg of
   
ChangeName newName -> ( { family | name = newName }, Cmd.none )
   
Save -> ( model, saveFamily family )


selectedView family
=

    UI
.form
       
[ UI.class "fluid"
       
, UI.onSubmit <| SaveFamily model.family
       
]
       
[ UI.input
           
[ UI.label "Name"
           
, UI.value model.family.
name
           
, UI.onInput ChangeName
           
]
       
]


-- Update.elm
update msg model
=
 
case msg of
   
FamilyMsg familyMsg ->
      let
       
(newFamily, familyCmd) = Family.update familyMsg model.family
     
in
       
( { model | family = newFamily }, Cmd.map FamilyMsg familyCmd )



This is how we organise the code in production and it scales really well; it also has the advantage of removing as much logic as possible from the view and keeping it in the update.
The last block's boilerplate can be further reduced using an helper like `mapBoth` from http://package.elm-lang.org/packages/Fresheyeball/elm-return/latest (or implementing it yourself, it's a couple of lines).

Martin Bailey

unread,
Mar 5, 2017, 6:41:26 PM3/5/17
to Elm Discuss
Hi Francesco, thanks for sharing the example.

I agree that's a good structure for different sections of an app. I oversimplified the first example to show the current necessity of updater functions. The more painful part is deeply nested records such as in my last Geocode example. I feel it would be cumbersome creating layers of messages and OOP-style updaters to mimic the data structure. Either you end up with 50 messages in the model update or you end up with 50 files for each record that can be updated.

I'd love to see examples of how to achieve the following without lots of boilerplate.

    RcvGeocode geocode ->
        ( { model | user.profile.contact.address = extractAddress geocode }, Cmd.none )

Witold Szczerba

unread,
Mar 5, 2017, 7:41:25 PM3/5/17
to elm-d...@googlegroups.com
Either you end up with 50 messages in the model update or you end up with 50 files for each record that can be updated.

There is no need to split or combine messages because of update function. The other solution is to create a `mapX` function for each nested structure and use it for each update:

-- /Feature/Announcements/AnnouncementItemEdit.md

    BodyEdit body ->
        mapAnnouncementItem model -- this is top level model
            (\item -> { item | body = body })
            ! []

    DurationEdit duration ->
        mapAnnouncementItem model -- this is top level model
            (\item -> { item | duration = duration })
            ! []

-- /Model.elm

type Model
    = Blank
    | AnnouncementListPage (WebData (List Announcement))
    | AnnouncementItemPage (WebData AnnouncementForm)

The top `Msg` has just one tag for entire announcement item edit form, the view/update functions always return top level models and messages.

Regards,
Witold Szczerba


--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

Michael Jones

unread,
Mar 6, 2017, 10:09:49 AM3/6/17
to Elm Discuss
Nothing special and I'm a beginner so I might be doing something stupid but I'm attempting to have some nested data in my model and this is the current state of one of my update functions which is fairly painful.

update : Business.LocationList.Model.Msg -> Model -> ( Model, Cmd Business.Model.Msg )
update msg model =
    let
        locationListData =
            model.locationListData
    in
        case msg of
            SetName name ->
                let
                    data =
                        { locationListData | name = name }
                in
                    { model | locationListData = data } ! []

            Submit locationList ->
                let
                    results =
                        validateEditLocationListForm model.locationListData
                in
                    if List.length results > 0 then
                        let
                            data =
                                { locationListData | errors = Just results }
                        in
                            { model | locationListData = data } ! []
                    else
                        let
                            data =
                                { locationListData | errors = Nothing }
                        in
                            { model | locationListData = data } ! [ postUpdateLocationList locationListData locationList ]

            Response locationList result ->
                case result of
                    Ok value ->
                        let
                            locationLists =
                                List.map
                                    (updateLocationListFromResponse locationList.id model.locationListData)
                                    model.locationLists
                        in
                            { model | locationLists = locationLists }
                                ! [ Task.perform GoToUrl (Task.succeed <| url (ViewLocationListRoute locationList.id))
                                  ]

                    Err value ->
                        model ! []

Francesco Orsenigo

unread,
Mar 6, 2017, 4:49:02 PM3/6/17
to Elm Discuss
@Martin Bailey

if I found myself with 50 messages in the model update I'd take as a sign that I need to abstract things more.

Regarding the update of nested attributes, I agree that there is not a good way to do it, hence this thread. =)



@Michael Jones

Maybe something like this could work for you:

    case lol of
       
Submit locationList ->

            let
                results
=
                    validateEditLocationListForm model
.locationListData

               
( newLocationListData, cmd ) =

                   
if List.length results > 0 then

                       
( { locationListData | errors = Just results }, Cmd.none )
                   
else
                       
( { model | locationListData = data }, postUpdateLocationList locationListData locationList )
           
in
               
( { model | locationListData = newLocationListData }, cmd )




Charlie Koster

unread,
Mar 6, 2017, 7:52:14 PM3/6/17
to Elm Discuss
Here's my example from an attempt to approach this topic in a similar way.

My use case is my data is modeled in a “has a” kind of way. I have rounds and I have lightning talks. A round has a date, it has a theme, and a round also has a collection of lightning talks. I am very often writing boilerplate for updating the nested lightning talk models because the primary domain object I’m usually dealing with is a round.


Here's a simplified snippet of code.


type alias LightningTalk =

  { title : String

  , description : String }



type alias Round =

  { talk1 : LightningTalk

  , theme : String

  , date : Float }



update msg model =

  case msg of

    UpdateTalkTitle newTitle ->

      let

        talk1 =

          model.talk1

          

        nextTalk =

          { talk1 | title = newTitle }

      in

        { model | talk1 = nextTalk }



I think there's room to reduce boilerplate in the highlighted code as well as make it more beginner friendly, since it's not obvious to store temporary results in temporary variables like talk1 or nextTalk.

Michael Taylor

unread,
Mar 9, 2017, 6:16:26 AM3/9/17
to Elm Discuss
I would like to extend upon Mark's suggestion of .field= functions that parallel the .field functions.

I find I want these as a pair more often than a setter functions on its own.

Lets use @field as an example syntax.

@field :
    { get : { a | field : b } -> b 
    , set : b -> { a | field : b } -> { a | field : b }
    }
@field =
    { get = \r -> r.field
    , set = \v r -> { r | field = v }
    }

This is in the spirit of arturopala/elm-monocle's Lens type, whose example:

streetNameOfAddress : Lens Address String
streetNameOfAddress =
    let
        get a = a.streetName

        set sn a = { a | streetName = sn }
    in
        Lens get set

would simplify to:

streetNameOfAddress : Lens Address String
streetNameOfAddress =
    Lens @streetName

I am using a similar lens type as part of an update composition library that allows simplification of:

update msg model =
    case msg of
        SearchPage msg_ ->
            let 
                (searchPage_, cmd) = SearchPage.update msg_ model.searchPage
            in
                ( { model | searchPage = searchPage_ }, cmd )

into:

update msg model =
    case msg of
        SearchPage msg_ ->
            Compose.update msg_ model @searchPage SearchPage.update

The above example of Compose.update is highly simplified, it can do much more than just reduce boilerplate code.  I have reduced the example to highlight to focus on the @field syntax.

Compose.update can only reduce boilerplate code if @searchPage does not have to be expressed as:

{ get = .searchPage, set = \v r -> { r | searchPage = v } }

To this end I have a pre-elm-compile script that looks for expressions of the form At.field and builds an elm source file of the form:

module At exposing (field, name, searchPage)

field = { get = .field, set = \v r -> { r | field = v } }
name = { get = .name, set = \v r -> { r | name = v } }
searchPage = { get = .searchPage, set = \v r -> { r | searchPage = v } }

Fabien Henon

unread,
Mar 9, 2017, 9:44:06 AM3/9/17
to Elm Discuss
I have an update function that has to call update functions for each registered page. The update function of the page itself only returns the page model and the command, whereas the main update returns it's own model (all pages) and a command. Here is the painful code:

type alias Model =
    { home : Home.Model
    , about : About.Model
    }


type Msg
    = HomeMsg Home.Msg
    | AboutMsg About.Msg

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        HomeMsg msg_ ->
            let
                ( page, cmd ) =
                    Home.update msg_ model.home
            in
                ( { model | home = page }, Cmd.map HomeMsg cmd )

The code in the case block is long and painful when you have X case values.

So i tried to simplify this code with a function that maps the return value of the page update to the return value of the main update:

lift : (msg -> Msg) -> (Model -> a -> Model) (a, Cmd msg) -> (Model, Cmd Msg)
lift cmdMapper modelMapper (m, c) =
    ( (modelMapper m), (Cmd.map cmdMapper c) )

setHome : Model -> a -> Model
setHome model page =
    { model | home = page }

And in the main update case I have this line:

lift HomeMsg (setHome model) <| Home.update msg_ model.home context

This is a lot simpler but still I have to declare a function (like setHome) for each case value to update my model page field.

A better way to update model field more "dynamically" would have been great to only need 1 function and not one for each case value

Martin Bailey

unread,
Mar 9, 2017, 10:40:53 AM3/9/17
to Elm Discuss
Having built-in lenses would be great. I've been using evancz/focus for a while to allow the definition of generic form fields but it still required defining a Focus function for each record field.

For example, boolField : Model -> Focus -> String -> Html Msg

[ boolField model (record => fieldA) "Field A"
, boolField model (record => fieldB) "Field B"
]

Given a Model, it's easy to read a deep field but hard to update. If the same function would be able to both read and update a value, so much boilerplate would disappear, including the one that was necessary with the Focus library. We could write the following :

[ boolField model .record.deep.deeper.fieldA "Field A"
, boolField model .record.deep.deeper.fieldB "Field B)
]

Please ignore the fact that this could belong in an abstraction and not be updated directly from the Model. Abstraction is nice to model your domain and intent, but painful when it only serves to hide or spread boilerplate code. Replace Model with User and the same applies regarding nested records.

Brian Hicks

unread,
Mar 9, 2017, 10:46:29 AM3/9/17
to elm-d...@googlegroups.com
Martin, Mark: do you have concrete examples of painful record updates? It sounds like with the thought you're putting into this, surely you're experiencing some significant pain. Examples, the more real the better, would be super!

If you have suggestions, it would be more helpful to spin off a separate thread than discuss it here. :)

Alex Darlington

unread,
Mar 9, 2017, 11:43:01 AM3/9/17
to Elm Discuss
I really want to just write something like:
newmodel = { model | field.deeper.deeperer = "new value" }

People have pointed me towards something Evan wrote about discouraging this, but I still don't understand why it would be a bad idea.
It would reduce quite a bit of boilerplate.
At the moment it just encourages me to keep everything flat, so rather than having a nested record I will do something like make lots of fields so I can write things like:
newmodel = {model | field_deeper_deeperer = "new value" }
Terrible, I know.

Mark Hamburg

unread,
Mar 9, 2017, 6:09:22 PM3/9/17
to Elm Discuss
I got asked for non obfuscated/simplified examples, so let me run through some more realistic code. I would pull from our own codebase but we've often contorted ourselves away from these patterns because they are so ugly so I can't just grab existing code.

Imagine having a style element that includes information about padding on each side:

type Rect = { left : Int, top : Int, right : Int, bottom : Int }

type Style = { padding : Rect, ... }

Expressing it this way allows us to work with the padding as a whole when useful and to avoid having to work with field names like leftPadding, topPadding, etc (presumably along with leftMargin, topMargin, etc) 

But now say one wants to change just the left padding. The code has to build a new padding object and then stick it back into the style. A little ugly from the standpoint of repetition, but maybe just...

{ style | padding = { style.padding | left = 10 } }

Nope. We can't use an expression for the record to be updated. So, instead we have to write something more like:

let
    oldPadding =
        style.padding
    newPadding =
        { oldPadding | left = 10 }
in
    { style | padding = newPadding }

Or we can use the successive wrapping pattern if we have the necessary utility functions:

10 |> asLeftIn style.padding |> asPaddingIn style

And the inability to use an expression to derive the record also means that we can't do something like:

{ Style.default | color = "red" }

But rather have to write:

defaultStyle : Style.Style
defaultStyle = Style.default

...

{ defaultStyle | color = "red" }

I don't have a good answer to the ugliness of the nested update though the successive wrapping pattern would probably be pretty reasonable if we had .field= in addition to .field. But the latter problem seems like the sort of thing that should be very straightforward to solve and would also allow for writing the first version of updating the left padding that I wrote above.

Mark

To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+unsubscribe@googlegroups.com.

Andrew Radford

unread,
Mar 10, 2017, 9:33:19 AM3/10/17
to Elm Discuss
Great post Mark - I've been working on a project lately and setters are fast becoming my #1 pain. I can relate to every one of those points you outlined, and you said it better than I could.

Mark Hamburg

unread,
Mar 18, 2017, 1:26:57 PM3/18/17
to Elm Discuss
I just had a senior engineer on my team ask me whether it was really the case that putting a qualified name or a field access into the beginning of a record update would fail to compile. His examples were much like the other examples from this thread, but I'm noting this here to make clear that this seems to be a routine stumbling block.

Mark

Richard Wood

unread,
Mar 19, 2017, 6:54:05 AM3/19/17
to Elm Discuss
I have only recently begun immersing myself in Elm and have run into this quite early.

Here's what I want to do and it seems very reasonable: 

type alias Model =
    { signup : Signup
    }

type alias Signup =
    { email : { text : String, errors : String }
    , password : { text : String, errors : String }
    }
        
Update 
ValidateSignup ->
      let
              ( emailErrors, passwordErrors ) = getSignupErrors model 
       in
            ( { model | signup.email.errors = emailErrors, signup.password.errors = passwordErrors }, Cmd.none)

Here's the best I've been able to come up with

type alias Model =
    { signup : Signup
    }

type alias Signup =
    { email : ValidatableString
    , password : ValidatableString
    }

type alias ValidatableString = 
    { text : String, 
    errors : String 
    }

Update
...
        ValidateSignup ->
            let
                ( emailErrors, passwordErrors ) = getSignupErrors model 
                emailUpdate = ValidatableString model.signup.email.text emailErrors
                passwordUpdate = ValidatableString model.signup.password.text passwordErrors
                signupUpdate = Signup emailUpdate passwordUpdate
            in
            ( { model | signup = signupUpdate } , Cmd.none)
 
Please tell me I'm just missing something at this point, or advice for best practice would be very welcome.

art yerkes

unread,
Mar 19, 2017, 11:36:15 AM3/19/17
to Elm Discuss
It's not a huge deal but other strongly typed languages definitely allow arbitrary expressions in record update syntax.

# type t = { a : int ; b : float } ;;

type t = { a : int; b : float; }

# let emptyT () = { a = 1 ; b = 3.0 } ;;

val emptyT : unit -> t = <fun>

# let x = { (emptyT ()) with a = 7 } ;;

val x : t = {a = 7; b = 3.}

To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

Max Goldstein

unread,
Mar 20, 2017, 11:48:41 AM3/20/17
to Elm Discuss
@art, I disagree about adding arbitrary expressions between { and |. You should use a let-binding for something like Array.get (offset + i) arrayOfRecords |> Maybe.withDefault defaultRecord.

I know this is supposed to be pain points, not solutions, I'm going to try to coalesce some of the syntax proposals that have been brought up.

Qualified names as base records, such as {Module.defaultOptions | ... }. The default options pattern makes this very useful and it does not encourage nesting records deeply.

Dot-accessed records as base records. This would allow { element.padding | left = "4px" }. Having records as fields of other records is especially useful when the subrecord type can be reused (e.g. { padding : Rect, margin : Rect }.

Arbitrary expressions as base records. Allow anything of record type. I disagree with this one per above.

---------

Nested getter functions, so that .foo.bar is sugar for .foo >> .bar

Setter functions, perhaps with syntax .foo=

Nested setter functions, a combination of the two above, .foo.bar=

Setters that take a function given the current value of the field as an argument. padding |> .right@ (\padRight -> padRight * 2) This is particularly useful for mapping over lists instead of setting one value.

-----

I wonder if we could dispense with the vertical bar update syntax entirely if we had nested setters. Something like

Style.defaults
  |> .padding.left = 5
  |> .margin.top = 10

The compiler should specifically optimize this case so that many chained updates do not create intermediate records. (A downside is that you lose the ability to refer to the current record in the expression for the new value, but that record wouldn't exist because of the optimization. So that would mean you can't easily map over a list that's in a record.)

Robert Walter

unread,
Mar 22, 2017, 8:35:04 AM3/22/17
to Elm Discuss
Hi Richard,

this is not code that is in production yet and rather experimental, but I still think it might be worth sharing in the context of this thread since I had to write painful code to update nested records there (unfortunately, that code was never commited anywhere so I cannot share the ugly version, but I think you might be able to envision it given the model and the current code). I then came across an article by Wouter addressing the topic of updating nested records and I tried to apply it to my use case.
Let's start with the model that represents a series of paragraphs and a text selection:

type alias Model =
    { paras : Dict Int Para
    , selection : Maybe Selection
    }
type alias Para =
    { text : String }
type alias Selection =
    { cursorCoordinate : TextCoordinate
    , anchorCoordinate : TextCoordinate -- not in use yet
    }
type alias TextCoordinate =
    { textPos : TextPosition
    , location : Location
    }
type alias TextPosition =
    { iPara : Int
    , offset : Int
    }
type Location
    = Before
    | After

I am aware that there is probably a lot of room for improvement for more experienced Elm developers, but here is the "best way" I could come up with for an update method that "moves" a selection to the left (think "arrow key left" is pressed by the user):

dismantle : TextCoordinate -> ( Int, Int, Location )
dismantle tc =
    ( tc.textPos.offset, tc.textPos.iPara, tc.location )

moveLeft : Model -> TextCoordinate -> Maybe Selection
moveLeft model ({ textPos } as cursorCoordinate) =
    let
        ( offsetCur, iParaCur, locationCur ) =
            dismantle cursorCoordinate

        paraCur =
            Dict.get iParaCur model.paras

        cursorCoordinateNew =
            case paraCur of
                Nothing ->
                    -- this should never happen, since we should have a valid text coordinate at that point
                    cursorCoordinate

                Just para ->
                    let
                        paraLength =
                            String.length para.text

                        iParaAbove =
                            iParaCur - 1

                        offsetNew =
                            (offsetCur - 1)
                                |> cropTo -1 (paraLength - 1)
                    in
                        if (offsetNew < 0) then
                            if (paraLength > 0 && locationCur == After) then
                                -- we moved "after -1st" character: normalize to "before 0st"
                                textPos
                                    |> setOffset 0
                                    |> asTextPosIn cursorCoordinate
                                    |> setLocation Before
                            else
                                -- we moved "before -1st" character or we are in an empty para:
                                -- => normalize to "after last of prev. para"
                                let
                                    mbParaAbove =
                                        Dict.get iParaAbove model.paras
                                in
                                    case mbParaAbove of
                                        Nothing ->
                                            textPos
                                                |> setOffset 0
                                                |> setIPara 0
                                                |> asTextPosIn cursorCoordinate
                                                |> setLocation Before

                                        Just paraAbove ->
                                            let
                                                paraAboveLength =
                                                    String.length paraAbove.text

                                                offsetAbove =
                                                    paraAboveLength |> cropTo 0 paraAboveLength
                                            in
                                                textPos
                                                    |> setOffset offsetAbove
                                                    |> setIPara iParaAbove
                                                    |> asTextPosIn cursorCoordinate
                                                    |> setLocation After
                        else
                            textPos
                                |> setOffset offsetNew
                                |> asTextPosIn cursorCoordinate
    in
        createSelection cursorCoordinateNew

To make this work, I've written the "set***" and "as***In" methods as Wouter suggests in his article to complement my "model-API": 

setSelection : Maybe Selection -> Model -> Model
setSelection selectionNew modelCur =
    { modelCur | selection = selectionNew }


asSelectionIn : Model -> Maybe Selection -> Model
asSelectionIn =
    flip setSelection


setParas : Dict Int Para -> Model -> Model
setParas parasNew modelCur =
    { modelCur | paras = parasNew }


asParasIn : Model -> Dict Int Para -> Model
asParasIn =
    flip setParas
setText : String -> Para -> Para
setText textNew paraCur =
    { paraCur | text = textNew }


asTextIn : Para -> String -> Para
asTextIn =
    flip setText

setCursorCoordinate : TextCoordinate -> Selection -> Selection
setCursorCoordinate cursorCoordinateNew selectionCur =
    { selectionCur | cursorCoordinate = cursorCoordinateNew }


asCursorCoordinateIn : Selection -> TextCoordinate -> Selection
asCursorCoordinateIn =
    flip setCursorCoordinate


setAnchorCoordinate : TextCoordinate -> Selection -> Selection
setAnchorCoordinate anchorCoordinateNew selectionCur =
    { selectionCur | anchorCoordinate = anchorCoordinateNew }


asAnchorCoordinateIn : Selection -> TextCoordinate -> Selection
asAnchorCoordinateIn =
    flip setAnchorCoordinate
setTextPos : TextPosition -> TextCoordinate -> TextCoordinate
setTextPos textPosNew textCoordinateCur =
    { textCoordinateCur | textPos = textPosNew }


asTextPosIn : TextCoordinate -> TextPosition -> TextCoordinate
asTextPosIn =
    flip setTextPos


setLocation : Location -> TextCoordinate -> TextCoordinate
setLocation locationNew textCoordinateCur =
    { textCoordinateCur | location = locationNew }


asLocationIn : TextCoordinate -> Location -> TextCoordinate
asLocationIn =
    flip setLocation

setIPara : Int -> TextPosition -> TextPosition
setIPara iParaNew textPosCur =
    { textPosCur | iPara = iParaNew }


asIParaIn : TextPosition -> Int -> TextPosition
asIParaIn =
    flip setIPara


setOffset : Int -> TextPosition -> TextPosition
setOffset offsetNew textPosCur =
    { textPosCur | offset = offsetNew }


asOffsetIn : TextPosition -> Int -> TextPosition
asOffsetIn =
    flip setOffset


I hope this somehow helps to improve on the record update syntax. I'd love to have a possibility to not have to write above boilerplate in the future :) 
All credits for the above API goes to Wouter!

Best,
Robert

Matthieu Pizenberg

unread,
Mar 23, 2017, 2:23:13 AM3/23/17
to Elm Discuss
Here is a somewhat painful record update:

setImage : String -> String -> Image -> Model -> Model
setImage tool url image model
=
    let
        mapping
=
           
case tool of
               
"rectangle" ->
                   
.rectangle model.resourcesMapping

               
"outline" ->
                   
.outline model.resourcesMapping

               
"scribbles" ->
                   
.scribbles model.resourcesMapping

                _
->
                   
Dict.empty

        id
=
           
Dict.get url mapping
               
|> Maybe.withDefault -1

        config
=
            model
.config

        newConfig
=
           
case tool of
               
"rectangle" ->
                   
{ config | rectangle = Dict.insert id (NoGT url image) config.rectangle }

               
"outline" ->
                   
{ config | outline = Dict.insert id (NoGT url image) config.outline }

               
"scribbles" ->
                   
{ config | scribbles = Dict.insert id (NoGT url image) config.scribbles }

                _
->
                    config
   
in
       
{ model | config = newConfig }

Yosuke Torii

unread,
Mar 24, 2017, 12:20:42 AM3/24/17
to Elm Discuss
I have two real-world examples. Nothing is modified at all.


1. Update inner structure

canvasPosition
: Model -> Position
canvasPosition model
=
  let
    pos
=
      model
.mousePosition
 
in
   
{ pos | y = pos.y - headerHeight }



2. Add field to make extended record (cannot use update syntax)

type
alias Note =
 
{ position : Int
 
, note : Int
 
, velocity : Int
 
, length : Int
 
}


type
alias Detailed a =
 
{ a | track : Int, channel : Int }


addDetails
: Int -> Int -> Note -> Detailed Note
addDetails track channel note
=
 
{ position = note.position
 
, note = note.note
 
, velocity = note.velocity
 
, length = note.length
 
, channel = channel
 
, track = track
 
}



2017年3月3日金曜日 15時12分39秒 UTC+9 Richard Feldman:
Reply all
Reply to author
Forward
0 new messages