Frustrations with understanding tasks and ports

1,089 views
Skip to first unread message

Sridhar Ratnakumar

unread,
May 18, 2015, 3:06:32 AM5/18/15
to elm-d...@googlegroups.com
"To actually perform a task, we hand it to a port.”

"Ports are a general purpose way to communicate with JavaScript.”

Huh? Come on! They two are not even conceptually related! 

What I’m trying to do is *actually* run a task from the “update” function of Evan’s Start-App framework. So I have code like this:

update : Action -> Model -> Model
update action model =
  case action of
    Reload ->
      -- TODO: actually perform a task (POST request) here
      newModel

I know how to create the Task value, but how do I *perform* it? The documentation says “hand it to a port” - but from all the code and examples I have seen so far a “port” is something you define at a global level in a module (and the runtime magically runs it, on startup maybe). How in the name of $deity do I actually perform a task dynamically, from deep inside a pure function like the above?

somewhat-frustrated-but-trying-to-be-cheerful,
-srid

Evan Czaplicki

unread,
May 18, 2015, 3:17:41 AM5/18/15
to elm-d...@googlegroups.com
This is covered by the "one last pattern" section, but I don't think it's been fleshed out in any nice document at this point.

This kind of came up a little while ago here.

The idea is that you never DO the effect in your component. If you want to talk to a database, you expose as part of the components public API (1) a type that models these requests and (2) a way to turn these models into tasks that are run at the top level. Okay, why is that good though?

Testing
  • In world A your component is directly hooked up to the database. If you want to do any tests you have to do crazy things to your code to swap in a dummy database.
  • In world B your component exposes an abstract DatabaseRequest type and a (run : DatabaseRequest -> Task x a) function. Now when you test, you can just write your own (testRun : DatabaseRequest -> a) that mocks the behavior you want.
Caching / Batching
  • In world A, 4 of your components need access to the users profile picture. None of them can know about each other (modularity!) so the all have to make a request separately even if the image is already loaded.
  • In world B your 4 components all expose a UserRequest that you can combine with (batch : List UserRequest -> Request) before sending.
This example might work better for talking to databases where you want things to be atomic or to never have a case where a partial transaction is possible.

That said, this whole idea has not been super well explored or documented. Does it make sense though? Are there any steps the community can take on this?

--
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.
For more options, visit https://groups.google.com/d/optout.

Hassan Hayat

unread,
May 18, 2015, 4:06:01 AM5/18/15
to elm-d...@googlegroups.com
The update function is not where you run your tasks. Think of your update function as just a way to transform your state into a new state. If you imagine your state as one big JSON object, then your update function just takes a JSON object and produces another one. That's it. 

The thing with the update function is that it takes an input or action. So, the trick is that you have a mailbox of actions and you have a task send some values to that mailbox's address. This will update the mailbox's signal with a new value and this is where you get your actions for the update. 

So:

Task gets performed -> Task returns with a result -> Task sends result to mailbox address -> Mailbox signal updates -> rinse and repeat


This explains you go from tasks to actions, but how do you actually perform those tasks?

  1. Create a mailbox for the task you want to perform 
myTaskMailbox : Mailbox (Task error value)
myTaskMailbox
= mailbox myTask

This will allow you to send tasks to this mailbox. When tasks get sent to the mailbox, provided a port was opened, the task will be performed. 


      2. Open a port with the signal from the task mailbox

port myTaskPort : Signal (Task error value)
port myTaskPort
=
  myTaskMailbox
.signal

If you don't include this, the tasks won't get performed. This basically tells Elm that you're trying to do something effectful or impure (like making an HTTP request or getting data from a database).

This is exactly what happens when you try to talk to JS. From Elm's perspective, JS is like this black box. The relationship between Elm and JS is basically the same as that between a browser and a server. Communicating with one another requires effectful/impure computations and the only thing they exchange with one another is data. This is why they both share the port syntax. From Elm's point of view, JS is just one possible source of effects among many. 

The cool thing about this is that if you comment out the port, the tasks won't perform. The guarantee Elm provides is that ports declare which effects you're performing. No ports, no effects, just pure computation.


         3. Send tasks to this mailbox. 

You can send tasks from many places:
  1. When you create a mailbox, you get to send a task when the program starts as shown above.
  2. You can send tasks from within tasks. 
  3. You can send multiple values from multiple sources to multiple addresses from within a single larger task where each intermediate task in interspersed by sleep calls or just wait for a task to complete. For example, a single task can : Fetch data from a server, parse that data, send the parsed data to an address, wait for 1 second, fetch data from a database, perform different tasks based on the result, etc... You can get quite involved with the tasks logic, except that tasks on their own are not that useful. If you want to actually change your application state, you have to send values to an address.
  4. You can send tasks from an event handler (like Html.Event). Use this feature carefully, but the gist of it is that you can have a button trigger an HTTP request. (onClick myTaskMailbox.address myAwesomeHttpRequest). So, basically, you can send tasks from your view. Again, if that port wasn't opened, the task won't get actually performed. 

Here are a couple examples to help you out. They both showcase tasks.

The first example is a mini hacker news feed reader: https://gist.github.com/TheSeamau5/1dc5597a2e3b7ae5f33e

It's not quite dynamic in the sense that you send tasks based on events, but it does have to send multiple http requests. Basically, it sends a request to fetch a JSON file to that contains the locations of the articles to display and then perform all of those requests. If you scroll down, there's a write-up/tutorial that even explains how to get the requests to load in parallel. 

The second example is an artist search example. https://gist.github.com/TheSeamau5/98527f7278a2c6f06092

This one doesn't have an update function but does a lot of task stuff. This should kinda give you the idea that tasks really don't go in the update function.

The last example is a reddit home page viewer. https://gist.github.com/TheSeamau5/80fdcb8c2f7e599c5099 

This one performs tasks in event handlers and when the program starts. If the page fails to load, you get a button to retrigger the loading of the page. So, you're sending a task from the button. 


I hope these examples help a bit. Tasks are still new in Elm, so the best practices haven't fully formed yet, but, to some up, the idea is this.

Create a mailbox for your tasks.
Open a port with the signal of the task mailbox.
Send your tasks to this mailbox.
You can send tasks when the program starts, from within other tasks, or from event handlers.
Send a value to an address is a task, so use tasks to send data to other mailboxes, i.e. mailboxes for actions, not tasks.
Update deals exclusively with actions and state. It is a pure function that given an action and a current state produces the next state. 

I hope this helps, 
Hassan

Sridhar Ratnakumar

unread,
May 18, 2015, 3:45:33 PM5/18/15
to elm-d...@googlegroups.com
Evan & Hassan - thank you so much for the elaborate responses. I’ll mull over them further[1] tonight after my various errands and respond here with my thoughts.

cheers,
-srid

[1] I did a brief thinking over your emails, while reading them on the phone today, and I was yet to find out how to make the Signal.map in start-app’s `start` function “send” the top-level Tasks (from Request run’s) especially as the very sending requires a port to actually happen! I suppose I should fork start-app and adapt it to use `update : Action ->  Model -> (Model, Maybe Request)` and point out where exactly I have trouble connecting them all to a top-level port. Will get back to this soon tonight.

dedo

unread,
May 18, 2015, 4:48:31 PM5/18/15
to elm-d...@googlegroups.com
Thanks Hassan, that was helpful. Elm needs a clear description of this.

Off topic -- how do you add markdown documentation to your gists?

Hassan Hayat

unread,
May 18, 2015, 4:56:45 PM5/18/15
to elm-d...@googlegroups.com
Off topic -- how do you add markdown documentation to your gists?

I added the markdown in a separate file. When you create or edit a gist, look at the bottom left (beneath the code area). There should be an "Add file" button. I then give that file a .md extension and voila! markdown.

Sridhar Ratnakumar

unread,
May 19, 2015, 1:22:31 AM5/19/15
to elm-d...@googlegroups.com
On Mon, May 18, 2015 at 12:17 AM, Evan Czaplicki <eva...@gmail.com> wrote:
This is covered by the "one last pattern" section, but I don't think it's been fleshed out in any nice document at this point.

This kind of came up a little while ago here.

The idea is that you never DO the effect in your component. If you want to talk to a database, you expose as part of the components public API (1) a type that models these requests and (2) a way to turn these models into tasks that are run at the top level. Okay, why is that good though?

Testing
  • In world A your component is directly hooked up to the database. If you want to do any tests you have to do crazy things to your code to swap in a dummy database.
  • In world B your component exposes an abstract DatabaseRequest type and a (run : DatabaseRequest -> Task x a) function. Now when you test, you can just write your own (testRun : DatabaseRequest -> a) that mocks the behavior you want.
Caching / Batching
  • In world A, 4 of your components need access to the users profile picture. None of them can know about each other (modularity!) so the all have to make a request separately even if the image is already loaded.
  • In world B your 4 components all expose a UserRequest that you can combine with (batch : List UserRequest -> Request) before sending.
This example might work better for talking to databases where you want things to be atomic or to never have a case where a partial transaction is possible.

That said, this whole idea has not been super well explored or documented. Does it make sense though? Are there any steps the community can take on this?

That answers half of my question. I can see the value of using Request and run at each level of component hierarchy, each of 'em wrapping the component immediately below.

But the question remains, as stated in [1] in the prior email, as to how to actually run these (wrapped) tasks? 

Reading Hassan's email - if the update function is not going to run the tasks, then I can only do this from event handlers (like this). However this conflicts with your design of returning a Request and then having some top-level component convert it to Task, because if sending tasks from event handlers the view will have to be invoking 'run'. 

Hassan, I looked at your reddit example. Would you be able to modify it to implement Evan's Request/run design above? I'm scratching my head as to how you can even possibly do this in Elm.

cheers,
-srid

Sridhar Ratnakumar

unread,
May 19, 2015, 1:30:39 AM5/19/15
to elm-d...@googlegroups.com
On Mon, May 18, 2015 at 10:22 PM, Sridhar Ratnakumar <sr...@srid.ca> wrote:
I can see the value of using Request and run at each level of component hierarchy, each of 'em wrapping the component immediately below.

But the question remains, as stated in [1] in the prior email, as to how to actually run these (wrapped) tasks? 

To translate that question to code speak, what should the XXX below (in the changes to start-app) be substituted for?

diff --git a/src/Util/App.elm b/src/Util/App.elm
index 5d714cf..bfa3f57 100644
--- a/src/Util/App.elm
+++ b/src/Util/App.elm
@@ -12,6 +12,8 @@ shockingly pleasant. Definititely read [the tutorial][arch] to get started!
@docs start
-}
+import Task exposing (Task)
+
import Html exposing (Html)
import Signal exposing (Address, Mailbox)
@@ -33,11 +35,12 @@ hard in other languages.
-}
-type alias App model action =
+type alias App model action request =
{ model : model
, view : Address action -> model -> Html
, update : action -> model -> model
, actions : Mailbox action
+ , runner : request -> Task String ()
}
@@ -74,9 +77,21 @@ start app =
address =
actions.address
+ runner =
+ app.runner
+
model =
Signal.foldp
- (\action model -> app.update action model)
+ (\action model ->
+ let
+ newModel, maybeRequest = app.update action model
+ in
+ case maybeRequest of
+ Nothing -> newModel
+ Just r -> let
+ task = runner r
+ in
+ howToSendThisTaskToPortXXX task)
app.model
actions.signal
in

cheers,
-srid


Evan Czaplicki

unread,
May 19, 2015, 1:54:14 AM5/19/15
to elm-d...@googlegroups.com
Get rid of the start function and App type and move stuff to the top level like in the elm-todomvc code. I think trying to fit everything in the start-app code is causing the friction. It began existing about a week ago, so it seems it has learning consequences we did not foresee.

Have you read up on signals? That is the glue that'll connect your state (foldp) to ports and main and everything else.

Sridhar Ratnakumar

unread,
May 19, 2015, 3:21:39 AM5/19/15
to elm-d...@googlegroups.com
On Mon, May 18, 2015 at 10:53 PM, Evan Czaplicki <eva...@gmail.com> wrote:
Get rid of the start function and App type and move stuff to the top level like in the elm-todomvc code. I think trying to fit everything in the start-app code is causing the friction. It began existing about a week ago, so it seems it has learning consequences we did not foresee.

Have you read up on signals? That is the glue that'll connect your state (foldp) to ports and main and everything else.

I read up on signals before, however I never 'grasped' it completely. I now got rid of start-app and moved those things to top level in Main.elm, and consequently I'm starting to get a hang of how ports and signals interact (I'm not bothering with mailboxes yet). You can see what I'm doing in the first two commits in this branch:

I'll reply again once I have something substantial – be it progress or hindrances.

thanks & cheers,
-srid

Sridhar Ratnakumar

unread,
May 19, 2015, 4:10:02 AM5/19/15
to elm-d...@googlegroups.com
Success! 

I have everything working now, even using Evan's "Maybe Request" returning update function. See the full diff here:


The creation of several "Request" types (one for almost every component), and then wrapping all the request types layer by layer seems a little complex though. I need to look into simplifying the code one day. Also, as of now, one component (FeelingEdit) is re-using the Actions from another component (FeelingEdit), which needs to be rethought. 

cheers,
-srid

Evan Czaplicki

unread,
May 19, 2015, 4:28:07 AM5/19/15
to elm-d...@googlegroups.com
Awesome! Sorry for the friction, and I'm very glad it's working now!

I think the question of "how many Request types should there be?" is a great one, and I feel it depends on your particular application. There are two extremes:
  • Every component has it's own Request type. You wrap it up as you go higher in the hierarchy. This means each component only can make requests it has defined locally.
  • There is one Request type for the whole application. This means you can just pass it up, no problem. The downside here is that if one component can print, that capability can be used by any component in the whole system.
The latter option is easy. The question is, how precise do you want to be about requests? Do you want very strict permissions? If so, start breaking things up. You can slide towards the first option and find a nice balance of sharing and specificity. I think we'll see a best practice guideline on this emerge as more people do it!

Does that make sense?

--

dedo

unread,
May 22, 2015, 10:17:15 PM5/22/15
to elm-d...@googlegroups.com
@Sridhar, I'd be very interested in your own summary of tasks / ports / signals / mailboxes that you think might be more clear ... while the head-scratching is still fresh in your mind. 

Thanks!

Sridhar Ratnakumar

unread,
May 24, 2015, 10:57:30 PM5/24/15
to elm-d...@googlegroups.com

On May 22, 2015, at 7:17 PM, dedo <dedo...@hotmail.com> wrote:

@Sridhar, I'd be very interested in your own summary of tasks / ports / signals / mailboxes that you think might be more clear ... while the head-scratching is still fresh in your mind. 

Thanks for the prompt, dedo. I have been meaning to write this up as a blog post and finally got the impetus to do it:


Everyone, please feel free to point any errors in my thinking here.

cheers,
-srid

Texas Toland

unread,
Jul 4, 2015, 1:31:39 PM7/4/15
to elm-d...@googlegroups.com

think might be more clear ... while the head-scratching is still fresh in your mind.

I just read the Tasks tutorial myself. Aside from Reactivity being an odd name it wasn't clear to me how to defer starting a Task from a port. This thread connected why a port would accept a Signal of Tasks. The examples in the tutorial show 1) counting synchronously and 2) HTTP at page load. HTTP arbitrarily after page load might clarify practical use.
Reply all
Reply to author
Forward
0 new messages