The Author Online Book Forums are Moving

The Author Online Book Forums will soon redirect to Manning's liveBook and liveVideo. All book forum content will migrate to liveBook's discussion forum and all video forum content will migrate to liveVideo. Log in to liveBook or liveVideo with your Manning credentials to join the discussion!

Thank you for your engagement in the AoF over the years! We look forward to offering you a more enhanced forum experience.

Wouter (3) [Avatar] Offline
#1
Hi Richard,

Thanks for taking the time to write this book! I've really enjoyed it so far.

Coming from a pretty solid FP background, I noticed you made a certain design decision in Chapter 4 that I would have made differently. As you introduce the possibility for failure, you introduce a new field in the Model, loadingError, along with a few other more superficial changes. One drawback of this choice is that any later update to the model can set this field -- even after the loading has succeeded. Following the slogan 'make impossible states impossible', I would have expected something like this:

type Model =
  Loaded LoadedModel
  | Loading
  | Error String


(The LoadedModel is essentially the same model as in Chapter 3).

This has the advantage of being able to re-use all the previous code for handling the model once it has been loaded pretty much out of the box. The only new code basically encodes a little state machine, explaining how to transition from the initial Loading state to either the Error state or the Loaded state. The logic here is pretty straightforward -- I'd be happy to share my complete solution if you're interested.

I'd be really interested in your thoughts! Thanks in advance,

Wouter
rtfeldman (60) [Avatar] Offline
#2
Oh I completely agree! This is how I'd design it first if I were building it for production.

The reason I didn't do it this way in Chapter 4 is that I want to introduce that concept later. I figure HTTP is enough to digest for one chapter without diving into data modeling techniques as well.

I definitely plan to refactor that part of the Model later on in the book. smilie
Wouter (3) [Avatar] Offline
#3
Hi Richard,

Thanks for the quick reply! It's nice to hear you agree smilie

Once you have a model corresponding to a union type, however, you start to see how certain Msgs only make sense if you're in one particular constructor of the Model. In this particular example, once the data is loaded, you want to forbid new LoadPhoto messages being issued. Is there a way to provide that kind of static guarantee in Elm? The closest I can get is to write a wrapper around Msg with a phantom type variable. That way I can guarantee that only certain Msgs are sent from certain models, but the update function does not necessarily respect the typing discipline I'd like to enforce. In Haskell, I might try defining a GADT to provide stronger guarantees, but I can't see any better way to do this in Elm.

(Note: I don't want to criticize Elm for not having higher-ranked types, type classes, monad transformers, yadayada -- I understand and embrace the less-is-more philosophy)

Wouter

PS - I noticed that you sometimes to LoadPhoto as a (union) type constructor. Most other languages would call LoadPhoto a (data) constructor, where List or Maybe are referred to as type constructors. Is this a conscious choice?
rtfeldman (60) [Avatar] Offline
#4
Wouter wrote:Once you have a model corresponding to a union type, however, you start to see how certain Msgs only make sense if you're in one particular constructor of the Model. In this particular example, once the data is loaded, you want to forbid new LoadPhoto messages being issued. Is there a way to provide that kind of static guarantee in Elm?

In this example you could do something like `type Msg = LoadedMsg LoadedMsg | LoadingMsg LoadingMsg` and then have `update` be super short - just delegate to two other `update` functions, one to resolve `LoadedMsg` and the other to resolve `LoadingMsg`.

It'd still be possible to receive the wrong type of message for the wrong type of model, but that can't be ruled out at compile time (by Elm's compiler or any other) because it's just straight-up possible in the general case.

For example, suppose there's an async operation waiting to complete, but it doesn't resolve until after I've transitioned to the other flavor of model. Boom, mismatch between incoming message and current model. Inevitably we have to write code somewhere to handle that case, even if all we do is intentionally disregard the message.
PS - I noticed that you sometimes to LoadPhoto as a (union) type constructor. Most other languages would call LoadPhoto a (data) constructor, where List or Maybe are referred to as type constructors. Is this a conscious choice?
I basically ran that one by Evan and went with what he said. smilie
Wouter (3) [Avatar] Offline
#5
Hi Richard,

I agree that in general, it's hard to give any guarantees about which message is associated with certain models -- especially in the presence of asynchronous behaviour. In this example, though, it would still make sense to try and push the distinction between models into the view functions:

view :: Model -> Html Msg
view model = case model of
  | Loaded lm -> Html.map LoadedMsg (viewLoaded lm)
  | Loading lm -> Html.map LoadingMsg (viewLoading lm)

viewLoaded :: LoadedModel -> Html LoadedMsg
viewLoaded :: LoadingModel -> Html LoadingMsg


That way you can at least enforce that the two view functions cannot create messages that they really shouldn't. Even if you aren't sure when messages might arrive, you are restricting the kind of messages that can be created.

And regarding terminology: it's probably a matter of bracketing: I read 'union (type constructors)' rather than '(union type) constructors' -- but I'd probably avoid just 'type constructors' as in Figure 3.4, for example.

Thanks again for your thoughtful replies! I look forward to the rest of your book,

Wouter