What's a pattern for conditionally applying a transformation?

I’m writing a toy app that serves as a baseball scorekeeper. Right now I’m at the everything-is-in-one-big-record stage for my model. I’m writing a function which will represent a walk. Right now it has type

walk : Model -> Model

I have a helper function that returns a tuple of the new base configuration and a boolean representing whether a score was made. It has type

walkBases : List Base -> (List Base, Bool)

Ideally, I’d like to avoid this

let
  (newBases, scored) = walkBases model.bases
  intermediateModel = { model | bases = newBases } |> resetCount
in
if scored
then score intermediateModel -- score : Model -> Model
else intermediateModel

So I’ve come up with a little abstraction

-- Condition applies a transformation if cond is true
-- otherwise leaves a unchanged
condition : Bool -> (a -> a) -> a -> a
condition cond f a =
  if cond
  then f a
  else a

So now I can write

let
  (newBases, scored) = walkBases model.bases
  applyScoring = condition scored score
in
{ model | bases = newBases }
|> applyScoring
|> resetCount

Is there a pattern I’m missing here? I feel the domain may be hyper-specific to this, where a strike may cause an out which may cause an inning change, or a ball might cause a walk which might cause a score, but I feel this is a general problem to which smarter people have come up with a solution.

Thanks!

I’m not saying that one approach is better than the other, but here’s another possibility:

{ model | bases = newBases }
    |> (if scored then
            score

        else
            identity
       )
    |> resetCount

Edit: I just realized that your condition function can be written point-free:

condition : Bool -> (a -> a) -> a -> a
condition cond f =
  if cond
  then f
  else identity

Which makes my alternative an inlining of that point-free function.

1 Like

Thanks for the reply.

I’m not sure if this is “more generic” but I’ve ended up implementing this function

either : Bool -> (a -> b) -> (a -> b) -> a -> b
either cond ifTrue ifFalse=
  if cond
    ifTrue
  else
    IfFalse

condition is now just a special case of that function

condition cond f = either cond f identity

Since often enough I’m applying one thing or another (for example increment strikes or give an out) instead of purely conditional application. It feels like I’ve just reinvented the if-statement, but I find the helper function cleans it up a bit, kind like composing two functions instead of sending it through a pipeline twice. Maybe there isn’t a greater truth to be uncovered?

Just pointing out that either is just an alias for if-then-else. It has the disadvantage that both branches are evaluated, though in this case you’re constraining them to be functions, so the cost of either is unlikely to be large.

I don’t know anything about baseball, but I’m guessing the score is just a pair of integers, although in our walkBases function, you don’t seem to take which team is currently batting. Anyway, is it possible that you would be better off if walkBases also updated the score?

At the moment you’re returning a boolean which is essentially an instruction to either update or not the score. Why not just return the updated the score?

You seem to have something against “everything-is-in-one-big-record” but you don’t say why that is undesirable in this case. So I’m going to suggest that it’s perfectly fine, until you see something that is wrong with this. You can however, constrain your walkBases function:

type alias WalkBasesModel =
    { a | bases : List Base
    , homeScore : Int
    , awayScore Int
    , innings : Int 
    }
walkBases  -> WalkBasesModel a -> WalkBasesModel a
walkBases model =
   ...

Again I don’t know baseball, so I don’t know if you can figure out which score (home/away) to update just by knowing the innings number, or what you need for that.

You can also just have walkBases take in your whole model. I’ve found that taking in the whole model, means less ‘busywork’ of updating “model type aliases”. I previously had all of my view functions defining a 'Modeltype that specified only the things that thatviewfunction used. So for example, maybe theContactUspage, uses only a couple of fields from theModel. However, as I say, this results in quite a lot of 'busy work' when in reality, the vast majority of viewfunctions are not going to be re-used, either in the same project or another one. So there isn't really any harm in **most** of the view functions just taking inModel`.

One advantage might be that later, you might expand your application to allow scoring multiple games, and then fields such as homeScore and bases are fields on a baseball match not the model. But I would say, that’s a refactor you can do when the time comes. So I personally would probably just have:

walkBases  -> Model -> Model
walkBases model =
   { model
         | bases = ...
         | homeScore = ..
         ....
   }

If I ever later want to allow for multiple matches, it’s easy to change the type of 'walkBasesto accept (and return) aMatchrather than aModel`, and if you never do that then you haven’t wasted any time preparing for something that never came.

A final thought, a good exercise is to write down exactly why you think it is bad to have “everything-is-one-big-record”, if you find you’re writing vague/generic sounding platitudes such as “smaller records are better organised” (which is just a restatement of the claim you’re trying to back up), then you probably don’t need to change anything. If on the other hand you do find you can write some specific benefits to breaking up your “one-big-record” then that will guide you in how to break it up.

I acknowledged that in my post

I’m just questioning whether putting it in a function is worth the abstraction.

walkBases is a just a helper function to walk which updates the score, making it do both would just mean in-lining my helper function, which would make it very complicated since walkBases is a recursive function over the list of bases. Scoring is also dependent on other state - you correctly point out that it doesn’t know who’s at bat, because the model holds that information (top or bottom of inning drives home or away scoring) and updates the appropriate score (which itself is divided into score-per-inning).

I don’t believe that, nor do I believe I implied it. I meant to mention it in passing to communicate the shape of Model without pasting the whole thing, and also that it’s at an early stage in development, where most Elm apps I’ve seen grow beyond that eventually.

Well, walk does that, and walkBases is a helper function. There are also other reasons to score than getting a walk, so that’s already going to be broken out.

I’m still getting a feel for what code is “ugly” vs what code I just don’t know how to read yet . I appreciate your reply :slight_smile:.

When I see something like List Base I know you aren’t done with domain modelling. We can’t have 77 bases, but the type signature suggests that we can.

If the domain model is very strict, we cannot ever represent any impossible states. We can then better express legal transitions between states, and part of a legal transition would be a score change. No additional check is necessary after changing the state.

Below is a very strict representation of an inning, and scoring comes naturally as a result.

type BaseState
    = Occupied
    | Empty


type Diamond
    = Diamond BaseState BaseState BaseState


type alias Inning =
    { diamond : Diamond, outs : Count, strikes : Count, runs : Int }


type Count
    = Zero
    | One
    | Two


newInning : { diamond : Diamond, outs : Count, strikes : Count, runs : number }
newInning =
    { diamond = Diamond Empty Empty Empty, outs = Zero, strikes = Zero, runs = 0 }


walk : Inning -> Inning
walk inning =
    case inning.diamond of
        Diamond first second Empty ->
            { inning | diamond = Diamond Occupied first second }

        Diamond first second Occupied ->
            { inning | diamond = Diamond Occupied first second, runs = inning.runs + 1 }


type InningState
    = InPlay Inning
    | Complete Int


flyOut : Inning -> InningState
flyOut inning =
    case inning.outs of
        Zero ->
            InPlay { inning | outs = One }

        One ->
            InPlay { inning | outs = Two }

        Two ->
            Complete inning.runs
1 Like

Thanks for the reply! I have a few clarifications.

  1. I should have said this in my original post, but I’m not assuming any specific set of rules for “baseball” - there’s an adult rec league in town which adopts a modified rule set to speed the game. Two strikes is an out, three balls is a walk, but it’s still three outs for a half-inning, so I can’t rely on types that assume those rules. I’m also not assuming exactly three bases either.

  2. It’s unclear to me how your model accounts for the top and bottom half of the inning, and the fact that both teams play each inning. Does your inning model represent a half-inning? I think it is be a good idea.

  3. Assuming normal baseball rules, your walk function isn’t correct. A runner on second does not get to advance to third for a walk unless there is a runner on first. Likewise the runner on third only scores if the bases are loaded. Assuming a Diamond type the way you have described it, the only way to implement that function would be to enumerate all eight possibilities. (EEE → OEE, EEO → OEO, EOE → OOE, EOO → OOO, OEE → OOE, OEO → OOO, OOE → OOO, OOO → OOO + score).

My original post was mainly about conditional record modification. Do you have any advice there? Thanks!

I was trying to illustrate some ideas by example, and since the game at hand was baseball, that was the inspiration for my examples. I could have used a Soccer model as an example and made the same points. So it’s absolutely not important that I am modelling any (real-life) game correctly. Hopefully you can see there is no need to discuss your points. This isn’t a baseball forum.

Looking back at my example as an example, and not as an official representation of baseball, what is important is:

Because I am strict with modelling, and strict with transitions, it is easy for scoring to occur during state transitions and not as something to be dealt with as an afterthought.

This omits the need for the “do a thing, then check to see what I did, then conditionally do a thing based on what I just did” pattern.

Now, if it is infeasible to avoid the “do a thing, check to see what I just did” pattern, then I like to do something like this:

type Change a
    = NoChange
    | Changed a a


changeType : a -> a -> Change a
changeType a1 a2 =
    if a1 == a2 then
        NoChange

    else
        Changed a1 a2

Now I can do a case for a change (case (changeType model.bases nextModel.bases) of) AND pattern match to find only what I’m interested in AND only have the values I need in scope if necessary.

I didn’t mean to go into the minutia of modeling baseball, only relay enough information that the proposed solution is (1) incorrect and (2) not flexible enough. My point was that representing the state as an ADT is infeasible here, and even if I did express a Diamond as described it would force enumerating every combination. I thought by relaying that information I would provide enough for a different suggestion.

Your code example for Change a is intriguing, but I’m struggling to picture it in a wider code base. Do you have any examples of it you could link to? I think the “check to see if you did a thing” is really required here. A strike could result in an out, which could result in the end of an inning, which could result in the end of the game. Each of those events can also happen separately for other reasons, so I want to avoid repeating those cases.

I have an app that needs to inform the “outside JS world” of certain model changes. Problem is these changes can occur in any case of update messages. So the easiest way for me to handle this, is to compare models pre/post update.
My main looks like this:

main =
    Browser.element
        { init = init
        , view = view
        , update =
            \msg model ->
                let
                    ( nextModel, cmd ) =
                        update msg model

                    onChangeMessages =
                        onChange model nextModel
                in
                ( nextModel, Cmd.batch [ cmd, onChangeMessages ] )
        , subscriptions = subscriptions
        }

update is implemented as one might expect.
Here is a pared-down section of onChange that shows we want to send messages only when configurations change:

onChange : Model -> Model -> Cmd Msg
onChange lastModel currentModel =
    (case changeType lastModel.config currentModel.config of
        NoChange ->
            Nothing

        Changed _ NothingReady ->
            Just (readyStatus { virtual = False, hardware = False })

        Changed _ (VirtualReady _ _) ->
            Just (readyStatus { virtual = True, hardware = False })

        Changed _ (HardwareReady _ _) ->
            Just (readyStatus { virtual = False, hardware = True })

        Changed _ (HardwareReadyAndVirtualReady _ _ _ _) ->
            Just (readyStatus { virtual = True, hardware = True })
    )
        |> Maybe.withDefault Cmd.none

In your case, you’d probably have some pipeline where each step takes a model and some previous model and checks for changes (possibly using something like the above) and makes additional changes.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.