Skip to content

Instantly share code, notes, and snippets.

@jbrains
Created August 1, 2017 22:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbrains/1a4e2f4c22705e6d5cac6652db607bed to your computer and use it in GitHub Desktop.
Save jbrains/1a4e2f4c22705e6d5cac6652db607bed to your computer and use it in GitHub Desktop.
How do I move the composition into the pipeline operating on Maybes?
changePlayerName : PlayerModel -> String -> ScorekeeperModel -> ScorekeeperModel
changePlayerName playerToChange newName scorekeeperModel =
let
{ players } =
scorekeeperModel
in
{ scorekeeperModel
| players =
Exts.List.mergeBy
(.id)
players
[ { playerToChange | name = newName } ]
}
leaveEditingMode : ScorekeeperModel -> ScorekeeperModel
leaveEditingMode scorekeeperModel =
{ scorekeeperModel
| playerToEdit = Nothing
, newNameOfPlayer = ""
}
-- This is the part that feels weird. How do I inline this into the pipeline below?
changePlayerNameThenLeaveEditingMode : PlayerModel -> String -> ScorekeeperModel -> ScorekeeperModel
changePlayerNameThenLeaveEditingMode playerToChange newName =
changePlayerName playerToChange newName
>> leaveEditingMode
-- deep inside an update function
-- newNameOfPlayer is a String
-- playerToChange is a PlayerModel
-- scorekeeperModel is a ScorekeeperModel
-- this expression should return a ScorekeeperModel
scorekeeperModel
|> (newNameOfPlayer
|> Exts.Maybe.validate (not << String.isEmpty)
|> Maybe.map (changePlayerNameThenLeaveEditingMode playerToChange)
|> Maybe.withDefault identity
)
@jbrains
Copy link
Author

jbrains commented Aug 1, 2017

I want to be able to inline changePlayerNameThenLeaveEditingMode in this pipeline, but I can't figure out how to interleave the function composition with the Maybeness. I have the feeling that either (1) I'm looking for something that isn't there or (2) I'm missing something altogether different that I should be doing instead.

My overall strategy was to do this: parse the player name, Maybe.map (changePlayerName >> leaveEditingMode), and then apply this to the scorekeeper model, with identity as a default function to apply to the model. I'd like to do this as pointlessly (haha) as I can.

I looked into Applicatives, but I don't think I understood it. I was thinking that perhaps I could create a Maybe (changePlayerName >> leaveEditingMode) to apply to scorekeeperModel, and if function were Nothing, then I might just get back scorekeeperModel. I couldn't figure out how to do that. I didn't go far down this road; maybe I should have?

What would you do?

@SamirTalwar
Copy link

On a tablet so I can't test, but this feels right:

            |> Maybe.map (changePlayerName playerToChange)
            |> Maybe.map ((<<) leaveEditingMode)

@froderik
Copy link

froderik commented Aug 2, 2017

I would probably have done something similar. Another option is to let the functions used operate on Maybe ScorekeeperModel instead of ScorekeeperModel. Your pipe will be neater but your function interfaces more bloated. In my experience it is hard to get neat code all over the place so you will have to decide what parts of the code that benefits most from readability.

@stoeffel
Copy link

stoeffel commented Aug 2, 2017

You can just go with the simple approach. I think that's easier to understand.

if String.isEmpty newNameOfPlayer then
    scorekeeperModel
else
    changePlayerNameThenLeaveEditingMode playerToChange newNameOfPlayer scorekeeperModel

@Morendil
Copy link

Morendil commented Aug 3, 2017

Your types are not lining up, as far as I can tell.

One thing that I find helpful here is that the order of applying Maybe.map and function composition shouldn't matter.

That is, Maybe.map (f >> g) == (Maybe.map f) >> (Maybe.map g) (and that will hold true for << as well).

So looking at Maybe.map (changePlayerName playerToChange >> leaveEditingMode), I think of it as first using Maybe.map on each of the functions being composed, then composing the resulting Maybe functions. So for the whole thing to typecheck, each of the components has to typecheck.

The type of changePlayerName playerToChange is String -> ScorekeeperModel -> ScorekeeperModel - it takes a String and a ScorekeeperModel, then returns an updated ScorekeeperModel. We could also say "it takes a String, and returns a function that takes a ScorekeeperModel and returns a ScorekeeperModel". This latter phrasing seems odd, but it's the more appropriate one when we think of the Maybe type we have:

Exts.Maybe.validate (not << String.isEmpty) : String -> Maybe String

Because Maybe.map takes a function on base types, a -> b, and returns a function that turns a Maybe of type a into a Maybe of type b.

So Maybe.map (changePlayerName playerToChange) takes a Maybe String, and returns a Maybe (ScorekeeperModel -> ScorekeeperModel). (This is, in fact, just the type of changePlayerNameThenLeaveEditingMode playerToChange - which is why the code above works).

But it breaks down when you try compose that with

Maybe.map leaveEditingMode

Because the type of that must necessarily be

Maybe.map leaveEditingMode : Maybe ScorekeeperModel -> Maybe ScorekeeperModel

whereas for the composition to work we would need the type to be (ScorekeeperModel -> ScorekeeperModel) -> (ScorekeeperModel -> ScorekeeperModel).

What that type looks like to me is a function which takes a "ScorekeeperModel updater", and returns a different one. This is what @SamirTalwar's suggestion does - it gives to the second Maybe.map a function that says "compose something with leaveEditingMode". That something is also a "ScorekeeperModel updater", and they compose just in the way we need. (I think possibly you might want the arrow in the opposite direction, to apply leaveEditingMode last.)

HTH

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment