Intents and Facts - pondering CQRS in Elm

TL;DR

Sorry this turned out to be a lengthy one… The gist of it is: what about using separate types for messages that represent Facts and Intents instead of lumping it all into Msg and rely on convention and additional tests? If you’d like to know more have a :cookie: and proceed.

Edit: sorry the intial post had some huge copy&paste problems :smiley:

The Initial Spark

I recently had a chat on the Elm slack on how to make the Cmd msgs of your update results better testable. It turned out that a number of people have kind of the same ideas floating about their heads. The initial motivation for this is that once created we can’t inspect Cmds in Elm. Abstracting over the thing-to-do, in a way that is easily testable, would be to represent the Effect that the Cmd should achieve with a union type.

Here is a working Ellie of this initial approach that sports some helper code to adapt our new update function to the usual TEA layout, this can also be found on github.

-- (A) Before: one message type
type Msg
    = WhoIsTheKingInTheNorth
    | GotAnswer (Result Http.Error Character)

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        WhoIsTheKingInTheNorth ->
             let
                request =
                    Http.get
                        "https://anapioficeandfire.com/api/characters/583"
                        decodeCharacter
            in
            ( model, Http.send GotAnswer request )
-- (B) After: a message type and an `Fx` type
type Msg
    = WhoIsTheKingInTheNorth
    | GotAnswer (Result Http.Error Character)

type Fx = FetchJonSnow

update : Msg -> Model -> ( Model, List Fx )
update msg model =
    case msg of
        WhoIsTheKingInTheNorth ->
            -- This update result can easily be verified with a test
            ( model, [ FetchJonSnow ] )
        -- ...

evalFx : Fx -> Model -> ( Model, List (Cmd Msg) )
evalFx fx model =
    case fx of
        FetchJonSnow ->
            let
                request =
                    Http.get
                        "https://anapioficeandfire.com/api/characters/583"
                        decodeCharacter
            in
            ( model, [ Http.send GotAnswer request ] )

Note that the update function is now fully testable, so the intention of the-thing-to-do can be verified and supplied with data through an appropriate constructor. I wasn’t sure whether to go with the List (Cmd Msg) or just a plain Cmd Msg, which are pretty much both not really inspectable so using Cmd.batch would probably do no harm.

The whole exercise was so much fun that I continued to ponder what more we could do with this.

Voices in my head

While thinking about the nice improvement from (A) to (B) I became aware that the same approach could help to resolve another issue I’ve been thinking about lately. It always bothered me that most message based systems treat all messages equally when in reality more often than not they serve different purposes. In event-sourced systems you may involve Command-Query-Responsibility-Segregation (CQRS) meaning that you have commands and queries that you treat very distinctly although both are message types:

  • commands are messages the receiver can refuse to handle due to validation or other business assessments, this is how users/systems initiate a change of state in your application
  • while an event represents something that happened in your system in the past so it doesn’t make sense that you could even have the option to refuse to handle them, once stated it is immutable. It also doesn’t make sense that events could be introduced from the outside without a command, you want to protect your invariants, don’t you? The single source of truth in such a system is the eventlog - your current state at a specific point in time is a left-fold of all the events up until that point.

The Elm Architecture is one of the event-sourced systems that has only one message type, usually called Msg, which is nice from a usability perspective but the type system can not prevent you from handling messages in the wrong way, it’s all just convention and tests you need to know about/remember to write.

To illustrate the issue I see, let’s have a look at the update function which contains a mix of responsibilities, it handles:

  • messages that the user produces by interacting with the UI (i.e. through Html msg), I like to call them Intents which reads nice and serves to avoid the overloaded term command.
  • messages that an outside system has introduced via Subs - like Mouse.downs - which I would also categorize as an Intent to change the state of the system which needs to be checked by the system.
  • messages that are the outcome of some Cmd which typically represent the equivalent of events in CQRS. I like to call these Facts, again for readability and disambiguation.

I also find it strange that a view in TEA is allowed to produce any kind of message, if it type checks it’s OK, right?

-- (C) Did you mean to check first?
type Msg
    = CheckForRocketLaunches
    | SomebodyElseShotFirstLetsRetaliateImmedialtely

view _ =
    button
        [ onClick SomebodyElseShotFirstLetsRetaliateImmedialtely ] -- Oops
        [ text "Check for rocket launches around the world" ]

In light of these considerations I propose a more structured way to think about handling messages.

Intents and Facts

As I see it Facts are the only kind of messages that would go into your immutable eventlog and therefore would make up your state. Sometimes it is useful to also store the Intents of the users but to recreate your current state from scratch, a.k.a. the Model in Elm, a left-fold of your recorded facts is what you usually want. This has the huge benefit of being able to replay your events from scratch without the need to concern yourself with side-effects that might be introduced, they’re just facts after all. Intents serve the purpose of evolving your state in a more controllable way. By using them as the only means to let information flow into the system we could let the compiler handle a whole category of bugs we have to write tests for right now. Making impossible states even more unrepresentable :wink:. I tried to make Elm’s type system a more useful ally in segregating intents and facts so I came up with some helpers that make representing the appropriate distinction in types.

Here a Program wouldn’t handle generic Msgs but more specifically Intents to advance the state of the system. A working version can be found in this Ellie and this github gist.

-- (D) A bird's eye view on what we're doing
apply : Fact -> Model -> Model
interpret : Intent -> Model -> ( List Fact, List Effect )
produce : Effect -> Model -> Cmd Fact

main : Program Value Model Intent
main =
    Html.programWithFlags <|
        wrap
            { apply = apply
            , init = init
            , interpret = interpret
            , join = StateFact -- Needed to turn `Cmd Fact` into `Cmd Intent` for TEA
            , produce = produce
            , subscriptions = subscriptions
            , view = view
            }

type Intent
    = AskWhoIsTheKingInTheNorth
    | Incoming Value
    | StateFact Fact -- Really not happy about this, but seems necessary

type Fact = GotAnswer (Result Http.Error Character)

type Effect = GoFetchJonSnow

To initialize the system as with TEA you would need a default state and, going with the initial idea of separating Cmds from your normal update logic, a list of effects you want the Elm runtime to perform. I also found that having all the fact-handling in one place is nice so maybe we produce a list of facts we want to have applied to the initial state instead of doing it directly in the init function.

-- (E) Initializing our state
init : Value -> ( Model, List Fact, List Effect )
init flags =
    ( {}, [], [] )

Setting up subscriptions you might notice that messages from the outside will only be able go through our validation logic by producing intents (†).

-- (F) Safer subscriptions
subscriptions : Model -> Sub Intent
subscriptions model =
    Sub.batch
        [ toElm Incoming
        ]

Handling facts would be akin to what Html.beginnerProgram does. Note that in this iteration this is a dead-end in terms of control-flow, see trade-offs later on.

-- (G) Easier handling of plain facts
apply : Fact -> Model -> Model
-- Alternatively...? apply : Fact -> Model -> ( Model, List Intent )
apply fact model =
    case fact of
        GotAnswer (Ok { name, titles }) ->
            Debug.log (name ++ "/" ++ String.join "," titles)
                model

        GotAnswer (Err reason) ->
            Debug.log ("Couldn't find the answer: " ++ toString reason)
                model

Validating user/outside system intents would be the concern of another function. This would be the place to validate and refuse an intent if necessary by just returning ( [], [] ). Note also that if there wasn’t a technical need for the StateFact Fact intent the user/outside systems wouldn’t have the ability to just blindly state facts in your system without you validating their intents (†). This could be useful in filtering out unwanted Sub msgs coming from Mouse.moves and the like and you wouldn’t need to rely on convention to do this here because the types dictate that you can only do it here.

-- (H) All validation logic needs to go here and is testable
interpret : Intent -> Model -> ( List Fact, List Effect )
interpret intent model =
    case intent of
        AskWhoIsTheKingInTheNorth ->
            ( [], [ GoFetchJonSnow ] )

        Incoming json ->
            ( [], [] )

        -- Why won't you let me be?
        StateFact fact ->
            ( [ fact ], [] )

To produce an actual Cmd for the Elm runtime to run we would have another function that is only concerned with turning a user defined effect into what it actually amounts to in terms of TEA. This way we have pushed the not so testable code outside of our business logic, again without the need to rely on conventions. (I’m not sure that this is really necessary but for the sake of argument I’ll just include it)

-- (I) Finally producing a `Cmd` for the Elm runtime
produce : Effect -> Model -> Cmd Fact
produce fx model =
    case fx of
        GoFetchJonSnow ->
            let
                request =
                    Http.get
                        "https://anapioficeandfire.com/api/characters/583"
                        decodeCharacter
            in
            Http.send GotAnswer request

To complete the picture we have our view function that, interestingly enough, is now only allowed to produce intents, so the compiler would not let you just state facts from the user interface alone. Which might seem tedious for simple examples but I think that might be a good trade-off to make (†).

-- (J) Safer views
view : Model -> Html Intent
view model =
    Html.button
        [ onClick AskWhoIsTheKingInTheNorth
        ]
        [ Html.text "The King in the North!"
        ]

The Elephant in the Room

(†): The StateFact Fact constructor of Intent is a wart I wasn’t able to get around since you need a way to channel the Cmd Facts back into TEA via Cmd.map. Maybe the signature could be produce : Effect -> Model -> Cmd Intent but then i.e. GotAnswer would also need to be turned into an Intent which doesn’t seem right to me. Using the dreaded StateFact Fact escape hatch could alleviate the problem. Doing it this way would free us from having to cope with the control-flow-dead-end in (G), maybe it’s worth the effort?

-- (K) Potential alternative to returning a `Cmd Fact` and having to `Cmd.map`
produce : Effect -> Model -> Cmd Intent
produce fx model =
    case fx of
        GoFetchJonSnow ->
            let
                request =
                    Http.get
                        "https://anapioficeandfire.com/api/characters/583"
                        decodeCharacter
            in
            Http.send (\it -> StateFact (GotAnswer it)) request

Discussion

This has some very visible trade-offs:

  • There is obviously more stuff to set up and state handling would be fanned out to more functions than just update which might get unwieldy? To me it seems that the clearer separation would outweigh more complexity in bigger projects.
  • Except for intents and facts the terms are in a fluid state for me right now, so if you have any suggestions I’d love to hear about them!
  • I don’t like the term Effect - it looks and sounds too similar to Fact and is too generic to my mind - but I couldn’t find a better one: Ambition, Challenge, Consequence, Endeavor, Motivation, Procedure, Quest, Saga, SideEffect, Venture? Maybe Consequence would be a good replacement?
  • I’m currently not sure how you would model the “fetch something and then do this” workflow without the ability to sequence Cmd Facts chronologically, which Cmd.batch doesn’t guarantee AFAIK, changing the apply : Fact -> Model -> Model signature to apply : Fact -> Model -> ( Model, List Intent ), which would make applying Facts a little more awkward or producing Cmd Intent the way it is done in (K). Any suggestions?
  • Maybe having the need for a separate union type for describing the effect of a Cmd isn’t that necessary after all, I still like that it is more testable but I could imagine purging that to reduce the complexity overall.

Conclusion

The whole idea might just be a bad case of over-engineering since you can always separate functionality out into helper functions but my feeling is that having distinct types for messages that need to be treated differently from each other might be worth thinking about :slight_smile:.

I’m interested in what you think about this and how one could improve upon it? If you’ve come this far you really deserve another :cookie: or a :cake:

6 Likes

Couldn’t you replace StateFact with

type Msg fact intent = Fact fact | Intent intent

You then Cmd.map the results of produce and Html.map the results of view.

Mark

2 Likes

And going farther on your exploration, you might look at rules-based DCFT approaches:

http://web.stanford.edu/~ouster/cgi-bin/papers/rules-atc15

Here the effects would actually be produced based on the state of the model with facts (and presumably intents) in your factoring just changing the model state.

I haven’t tried converting Elm code to this structure but the paper reports strong success and I have built systems in the past that more or less followed this pattern and it definitely helped with reasoning because deciding what to do next — in Elm which commands/effects to issue — was separated from other state evolution.

In your example, I would imagine that this would lead to a field in the state containing something like:

type KingRequestState = NotAsking | Requested | Asking

The intent to get the King in the North would move this field from NotAsking to Requested but make no other changes. The rules for producing effects would generate the effect to get the king when the field value was Requested. And the effects to command conversion code would set the field to Asking when it produced the actual command. Both successful and unsuccessful GotAnswer responses would set the field back to NotAsking.

With everything broken down into intents, facts, and effects, and changes broken across interpretation of intents, application of facts, application of rules to produce effects, and conversion of effects into commands, it could easily become a bit much and might well be worthy of recombination into fewer parts, but from the standpoint of figuring out those parts, it seems like an interesting experiment.

Mark

Thank you for the paper, I will look into that for sure :slight_smile:. I might have some time at hand on the weekend, the abstract sounds really interesting,

As for the first comment:
I did not think about having a

type Outcome intent fact = Intent intent | Fact fact

type in the first place so I experimented around with it. As a user of a supposed library you could of course choose to use the aforementioned outcome as your application’s message type, which would deprive you of essentially all the benefits like that the type-checker will yell at you if you try to treat an intent as a fact in the system. In my view that would leave you only with the downsides like the verbosity and more union types to manage.

On the other hand: in trying to get rid of the StateFact intent the Outcome Intent Fact (provided by the library) approach really didn’t work out well since the overall type of the program would have to be Program Flags Model (Outcome Intent Fact). I’ve tried to contain the wrapper type to the produce : Effect -> Model -> Cmd (Outcome Intent Fact) signature but to get to my intended main : Program Flags Model Intent signature for the program you’d still need a way to produce facts as intents from that - back to where we started. Providing and relying on this as a library construct results in some serious downsides in my opinion:

  • As discussed we wouldn’t get around some kind of intent to state facts since TEA needs
    us to produce a Cmd of the type of message the program needs, if we want to keep the main : Program Flags Model Intent signature - which is the explicit goal
  • Views would also inherit the type Html (Outcome Intent Fact) which would let you
    state facts directly in the view, the very reason for the experiment in the first place. So I think it’d be better that the user herself provides an intent for stating facts. Maybe the user wants different intents to state varying kind of facts, who knows? I’ve certainly thought about needing that in my endeavor.
  • The muddied type signature would also be contagious to the outside world. One
    goal of the overall experiment is that an outside consumer of the program shouldn’t know about the implementation details that this specific library was in use, which would leak through the type signature. I’m also experimenting with composition of programs in the same prototype I’m building so I directly see how the library idiom bleeds through all the outside logic for creating partials.

What improved in my prototype is that by embracing the necessity that the user needs to supply means to state facts as intents I decided to go with the produce : Effect -> Model -> Cmd Intent signature. The library won’t care what kind of intent it is supplied, the user will explicitly use his own idioms to make sure the types line up. Right now this means that instead of supplying a join : Fact -> Intent function to the library the user will need to do it herself while producing the effect.

So after fiddling around with the idea I don’t see a proper way to get rid of the StateFact Fact intent, at least as long as TEA doesn’t explicitly support separate message types for these usages :wink:. Maybe that’s even the proper way to handle that scenario, my gut tells me there is something to this :blush:. It’s indeed an interesting topic.

Anyway, thank you for participating in my ramblings, I will get back to this topic when I’m finished with reading the paper and maybe cooking up a prototype. I’m trying to get the right balance between boilerplate, constraint through the type-checker, composability and expressiveness, I’m hyped…

So, this is me mostly playing around with words, but did you consider reversing the relationship between Facts and Intents? I admit I have not thought this through much and am not sure what case exactly this distinction would be useful in in Elm (a concrete, non-artificial example would sure help!). But it seems to make more sense semantically to have a fact stating that there was a user intent, than it is to say that a user intent is to state a past fact.

Well conceptually what I mean by Fact is a fact within the system not a fact outside the system, so

a fact stating that there was a user intent

to me is equivalent to just flowing the Intent into the system, if that makes sense? I realize the approach is missing a mission statement so my main thesis is:

  • Intents to change the state of the system should be handled differently than the facts inside the system comprising the state
  • The type-checker can help you with segregating these responsibilities so a user/outside system can not accidentally state in-system facts without the system being able to validate the intent to do so

What I inferred as technical constraints within TEA are:

  • The Msg type of such a system should be Intent, since that is the type of message coming in as Subs from outside systems and Cmds from user input and processes inside the system which need to be validated and eventually converted into facts within the system. In other words: intents are part of the public API where facts are not necessarily
  • Right now this should just be implemented in a TEA compliant library, no magic other than transformations. Library idioms should only be visible inside the application module so the type of the program needs to be Program Flags Model Intent.
  • As long as TEA doesn’t make the distinction between facts and intents the library needs a way to convert facts that are potentially generated by the Elm runtime via Cmds in order to satisfy the Program Flags Model Intent constraint. This is what I tried to get rid of and realized that it is probably unavoidable with using TEA or even a desirable trait of the system, since it lets the user decide what to do on a case-by-case basis.

Nice outcomes of using this library modulo the StateFact Fact idiom are:

  • All intents flow into the interpret : Intent -> Model -> ( List Fact, List Effect ) function, all message validation a.k.a. business logic is centralized there, the implementation details are handled by apply : Fact -> Model -> Model to replay facts to obtain the current state and produce : Effect -> Model -> Cmd Intent to advance the state of the system if necessary
  • The user can only flow intents into the system in view/other interaction declarations which both reduces the chances of triggering unvalidated logic directly from the view and also reduces the amount of logic the user is compelled to put into the view - the business logic lives in interpret
  • Outside systems can only flow intents into the system since the user of the library can choose not to expose Fact constructors outside of the module which better protects the system’s invariants.

Initially I thought bringing up a real world example would be too verbose but it seems it’d be helpful. I will come back to that. Sorry, this answer is another verbose one, have another :cookie: :innocent: thank you for your input, I appreciate it greatly.

I am only posting this because you posted this as a Request Feedback, and wondered out loud if this may be a case of over-engineering. In a nutshell, I think it might be!

When I first started coding in Elm, I went down the path of trying to “out think” TEA. I ended up creating a lot of complexity and it made debugging much more tedious. Also, it made the logic much harder to understand, especially when I was looking at the code a few weeks later.

Compare this:

https://ellie-app.com/jQ73XFVX6a1/0

to what you wrote. Super simple!

What to add more questions? Just add more types under Questions, then add a new branch in update for each AskQuestion and GotAnswer. Easy!

To me the beauty of TEA is the idea that there are four basic things:

  1. The Model, aka the state.
  2. Messages that can update the model, or as you say, facts, events, etc.
  3. An update function that takes a message and updates the model.
  4. Views that expose the current state/model, or queries.

A view is a query. It is saying “here, let me show some subset of the model in its current state”, in the same way a select in SQL shows you the current state of the DB.

A Msg is, in fact, an effect, regardless of who triggers that message–a user, a subscription, a mouse movement, etc. Those are all facts, so to speak.

The value you assign to those facts or messages or events is entirely up to you as the developer. There is nothing more or less special about a mouse movement, a user clicking on a button, a window being re-sized, a message from parsed from HTTP, or Cmd that is the result of something else happening. In my mind, there is no such thing an intent, there just a decision as a developer how to handle a given message. Or put another way, all messages are in fact an intent (to update the model).

To use SQL parlance, all messages in Elm are the equivalent of an update / insert / delete command, whereas a view is the equivalent of a select. Meaning, I don’t think you can subcategorize any messages per se as they all could potentially change the state of the model.

(Also, speaking purely practically, there have been many times where I wrote an update function that I thought wasn’t going to change the model, but later ended up needing to, and vice-versa, when I thought I wasn’t going to fire off a Cmd Msg, but then ended up needing to.)

Elm already has a category of things that can’t change the state: views.

The current state (e.g. model) is simply the result of running a sequence of messages through the update function (thus time-travel debugging), just as the state of a database is the result of running a series of SQL commands in an ordered sequence.

Also, maybe there is a difference between backend systems that may have buffering issues, network issues, “eventual consistency”, etc. vs. UIs where you really want to process all the events.

In other words, in a UI, all things have high value. I can’t imagine how Elm would work if any Msg didn’t get processed properly, or if any message was of lower or high value than any other. The state would be entirely indeterminate and Elm would fairly useless.

Maybe another way of saying this is that in the Elm world, all Msg's are “trusted”, as they are initiated within a closed system. This is not to say that the data itself is necessarily valid, but the actual event/message itself is.

When you run something on a server, the same rules don’t apply.

Also, I am saying this from the perspective of having developed a fairly large project in Elm. Our update function is 2,200 lines of code, and we have over 200 Msg types of all sorts: mouse-based, timer-based, internal Cmds, JSON parsing, HTTP parsing, WebSockets, form inputs, etc. Works brilliantly. Super responsive, super simple to understand the logic:

Msg -> update -> Model -> View

Anyway, just my two cents! Take with a grain of salt! Hope this helps!

2 Likes

Thank you for diving into the conversation. Here is my kurkuma to your salt :wink: - I’m playing devil’s advocate for the supposed library implementing the intents-and-facts concept, so please bear with me.

I’ve read your post a couple of times until I realized that the initial framing of the topic might be slightly off. Some of the observations from thinking about the apparent dissonance of what I picture in my mind, what came down in written and code form and what you got out of it in the initial post:

  • The post should probably have been named “pondering DDD in Elm” since all of what I’m referencing is only tied to the “Command” side of the CQRS pattern implemented in a Domain-Driven-Design fashion. The only query in the system is indeed the view, as long as there are no outgoing ports in the game.
  • As a corollary: if you’re doing CRUD there is no gain in using DDD, so this approach isn’t a silver bullet, nothing really is. So if you’re arguing that this is too much overhead for CRUD, I totally agree with you on that.
  • Therefore I’m no longer convinced that the intents-and-facts approach is an alternative to TEA but a library tool a developer can employ to gain a better grasp of what is going on in the system - which includes that the type-checker can help you with the distinction between intents and facts if she so wishes. This distinction can be very helpful if you have complex domain logic. These benefits come at the expense of terseness, I won’t argue with that, there are clearly trade-offs.
  • As it stands now with the current toy-implementation, the debugger may indeed be problematic as there is in fact only one Msg type visible from the outside: the Intent. Thank you for bringing that to my attention explicitly, I might have to channel all internally produced facts through a StateFact Fact intent to make sure these messages flow as normal messages through the system as well.
  • … although I still think the time-travelling debugger would benefit from knowing about the distinction between them. Right now in replay mode it has to throw away any Cmds generated by update. If it knew about the distinction it would just need to blindly replay facts and could provide a convenience option to correlate the intents to facts.

The SQL database analogy is very fitting, after all every system handling state is a database, I will take you up on that by pointing out my understanding of what the mapping between the proposed concepts and a real-world database is:

  • As already stated, I agree totally that the view is pretty much the only system-internal query in TEA. It’s the equivalent of having an XSLT middleware on your barebones HTTP DB interface of a materialized view in a relational database, there should be no business logic in that.

A Msg is […] an effect […] Those are all facts, so to speak

  • I agree with you that, from an outside view of the system, flowing a Msg into it could be seen as a fact. But here is where wording comes into play again and this is the reason why I wanted to use different terminology than is usually employed (i.e. events and commands). Facts how I see them are really domain-relevant-facts, truths that a domain-validation-handler has asserted at some point in time and what I mean by “validation” is the domain-logic that verifies the intent to change the system. And this is also why I want to make the distinction: your views contain no logic, how would they be in any place to state domain-relevant truths inside the system? They don’t know about the business logic, or at least they probably shouldn’t, they only display a read-view and capture the intent to do something. It doesn’t matter who or what the originator is, it could be the user via an Html Intent, or a Window.resizes subscription or a subscription coming through a port, who knows?

all messages in Elm are the equivalent of an update / insert / delete command

To further feed your SQL database analogy:

  • This is not how I see it, the SQL update / insert / delete commands don’t mean anything to the domain, they are an implementation detail. The correct analogy would be that a Fact is the meaning of something domain-relevant that happened in the system using domain terminology. Think PlaneSeatReserved instead of “insert a row into the reservations table”. What you’re attributing to all messages in Elm is just the implementation details the apply : Fact -> Model -> Model executes in order to materialize that meaning into the current state. The function contains no business logic, so to speak, the nitty-gritty details have already been entrusted to the domain logic in the first place so the system can be confident that there is no harm in replaying facts.
  • And here is where intents come into play. An Intent, as an implementation detail, may just be a Msg in TEA but to me it represents an intent to change the state which a domain-validation-handler has to handle. This handler is the interpret : Intent -> Model -> ( List Fact, List Effect ) function which should ideally be the only place where your business logic originates, it decides what to make of the intent. Do I produce domain facts from that intent or is it just not OK for me to do so in this moon phase, by the way what is the current moon phase, do I need to fetch that from somewhere via an Effect? Maybe the game is pausing so you decide not to channel all animation related messages through your system? Again going with the SQL database analogy: if you’re a relational database guru you would probably put that into a stored procedure - please don’t - or as a Java/C#/anything advocate another code middleware wrapping an HTTP request intended to mutate the state of your database, pick your poison. The Intent is the request to do something to the system.
  • So the point is not about assigning higher or lower values to the messages or arbitrarily leaving out messages, it’s about knowing that one is handled very distinctly from the other and making the type-checker your ally in keeping it that way. I agree that all messages are “trusted” in Elm as it is a closed system. But in my opinion it would be the same if you’d be on the server, it makes no difference. TEA is a beautifully crafted event based system that maps pretty well with many buzzword-bingo architecture candidates these days, I don’t feel that it is different from what you’d do on the server at all. But maybe that’s just me.

[…] speaking purely practically, there have been many times where I wrote an update function that I thought wasn’t going to change the model, but later ended up needing to, and vice-versa […]

  • I’m right there with you, only that this process would materialize in visible changes to the domain logic in interpret, you’d probably throw in some facts to change the current state of the system. Which are implemented by just some new cases in your apply function. The commit portion on your interpret function then should very likely make sense to a domain expert, if not you would at least need to answer a question or two from her which just might bring out another piece of information you didn’t think/know about in the first place.

In a DDD sense the ultimate goal would be that I could print out my interpret : Intent -> Model -> ( List Fact, List Effect ) function on a sheet of paper and sit down with a domain expert who could point out flaws in my understanding of how the system should work on a high level with her knowing little to nothing about Elm. Produced artifacts are not implementation details but domain-relevant-facts. This makes it also easy to test your business logic. That way we could pin down easily where implementation errors in the apply : Fact -> Model -> Model might be lurking and you could dive deeper on a per-case basis. Your customer would actually understand what the problem is and would be able to guide you to a better solution instead of just assuming your understanding is correct.

Which brings up the point of familiarity, I would argue that lumping your business logic together with implementation details about how to replay the current state from a message is not as easy to understand as you think. You are used to doing that but I like that the intents-and-facts way of thinking puts these two concerns into separate boxes, with some little help of the friendly compiler :slight_smile:

The bottom-line is: it might of course still be over-engineering but I’m going to try-out and refine my little helper library and see where it takes me. Thank you again for providing valuable input, your biological and technological distinctiveness will be added to my own :space_invader:. The discussions here on the Elm discourse are very inspiring to me, I’m spending way too much time here :blush: so have yet another :cookie:

1 Like

so much :salt:, :cookie: and :computer: is bad for the :heart: ahah but love reading those! Hooray for elm community. Enjoy the end of the weekend :wink:

1 Like

All work and no play makes Jack a dull boy… :running_woman: :wink:

I think maybe what you are trying to get at is that you see a ‘conceptual’ difference between a message like PlaneReserveSeat { reservationId, seat } vs WindowSize { width, height }.

If you want to make that distinction, I would just keep the update function as it is, but have a class of messages that pertain to whatever business logic you want to carve out.

E.g.

-- update module with business logic
module Update.Travel exposing (Travel, updateTravel)

type Travel
    = ReserveSeat BookingId PassengerId Seat
    | BuyTicket Date FlightId
    | CancelTicket BookingId
    | ChangeTicket BookingId
    | CheckFlightStatus BookingId
    | IsStillAvailable BookingId
    | Etc...

-- All business logic goes here...
updateTravel : Travel -> Model -> ( Model, Cmd Msg )
updateTravel travel model =
    case travel of
        ReserveSeat bookingId passengerId seat ->
            -- Business logic goes here...
            -- Return ( model, Cmd Msg )

        BuyTicket date flightId ->
            -- Business logic goes here...
            -- Return ( model, Cmd Msg )

        Etc... ->
            
------------------------------------------------------------
-- update module
module Update exposing (update)

import Update.Travel exposing (Travel, updateTravel)
import Window

type Msg
    = WindowSize Window.Size
    | UpdateTravel Travel

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        WindowSize { width, height } ->
            ( { model | windowSize = Window.Size width height }, Cmd.none )

        UpdateTravel travel ->
            updateTravel travel model

I had missed your constraint that the program signature needed to be stated in terms of intents. I was assuming that you were looking for a separation in which the UX produced intents while the model updates were driven by facts deriving either from those intents or from effects produced by the update logic. In other words, you don’t trust the UX but you do trust the logic inside the model/update combination including the results of any commands it issues via effects. That is a somewhat reasonable distinction to make in that the UX may fail to check for permissions (though it should). On the other hand, asynchronous commands may arrive after the context for their result has shifted, so trusting them could be argued to be dubious in a different way.

If you trust neither the UX nor the command results, then Intent is definitely your mechanism of choice and rather than having a generic StateFact intent, I think embracing the reasoning that stuff outside the system can’t be trusted means that you explicitly convert individual intents to specific facts rather than writing a generic loophole.

On the other hand, if you trust command results, then I think you are better off letting the program signature express that it works in terms of both intents and facts through a combined type and enforcing elsewhere — e.g., in a program constructor based on your factoring — that you actually expects views to generate intents while commands will generate facts.

Mark

I am making a sheet music editor (see https://parture.org) and had this thread open for months until I had some space to refactor my code with the Intents/Facts purpose in mind. I am pleased to say it was totally worth it and cleaned up my code a lot. Instead of having my update() clogged up with business logic to decide what to do with generic stuff that happened (like “SearchButtonClicked”) this was moved more explicitly to the View. Now it’s a design choice to say whether it’s something one likes or not, but I’m in favor to move the intents there more up-front.

Thanks!

1 Like

I’m glad this was helpful to you!