The problem

ports as they stand are fundamentally incompatible with Tasks. Being backed 
by Cmd's, they are harder to compose. A frustration of mine is that often 
we are directed to "just use ports" when a proper interface to some native 
API is not yet available, but this leads to our Msg types growing and more 
significant changes being required when eventually the proper interface is 
made available.

Also, many JS interop things I find myself wanting to do are fundamentally 
one-shot functions which I expect a result back into Elm from immediately, 
or otherwise just want to compose with other Task based things. Some 
examples that come to mind of one-shot tasks you may want to compose rather 
than use the streaming interface that ports provide:

   - Getting items from local/sessionStorage
   - .. really, most things involving working with the Web API that arent 
   yet implemented in Elm.
   - Embedding JS widgets into Elm elements
   - Using a JS library for doing things like hashing passwords or 
   obtaining some data back from some custom service
   - Interacting with things like Electron for creating apps that can run 
   in the desktop and interact with the filesystem etc.
   

The solution

Task ports. The idea is that these are defined the same way that Ports in 
elm currently are, but they return a Task type rather than a Cmd or Sub 
type. On the JS Side, we attach a function to the Elm app that returns a 
Promise, and on the Elm side we wait for the Promise returned to reject or 
resolve, and marhsall the error or result from the promise into the error 
or result type required by the Task type of the port.

Let's see how this might work:


*Ports.elm:*

port apiSession: Task String SessionId



*Main.elm:*

import Ports
import Json.Decode as Decode
import Task exposing (andThen)


-- get an API session from JS land and make an http request using it
-- given some path and a decoder to decipher the result:
apiRequest : String -> Decoder a -> Task ApiError a
apiRequest path decoder =
  let
    headers sessId =
        [ ("Content-Type", "application/json")
        , ("MyApp-SessionId", sessId)
        ]


    req sessId = Http.send Http.defaultSettings
        { verb = "POST"
        , headers = headers sessId
        , url = path
        }


    decodeResponse res = Decode.decodeString decoder -- ...handle error etc
  in
    Ports.apiSession `andThen` req `andThen` decodeResponse


*App.js:*

Elm.Main.ports.apiSession = function(){
    return new Promise(function(resolve,reject){


        var sess = localStorage.getItem("sessionId");
        if(!sess) reject("NO_SESSION");
        else resolve(sess);


    });
}

var app = Elm.Main.fullscreen();




Here, we use a tiny bit of JS to access localStorage and pull out a session 
ID. This function is used whenever the apiRequest Task is performed in Elm, 
and composes nicely into our apiRequest without the need for a complicated 
effect manager or threading a sessionId through everywhere just because we 
need to get it from a Cmd based port.

One of the nice things about this is that there is minimal refactoring to 
do for those things that do eventually receive coverage in the Elm Web API 
- you're just swapping out Tasks for other Tasks. As the Web API will 
always be changing, I think that having a nice way to make JS polyfills 
like this will always have some value, let alone for interacting with 
libraries written in JS that haven't or won't ever be ported to Elm.

Elm would continue to make the same guarantees as with other ports; if the 
task port can't marshall the response back into Elm an error would be 
thrown along the same lines as is currently done via ports.

Summary

- regular ports only let you send data off or receive data back, not both.
- Cmd's and Sub's are not composable
- Task based ports allow you to create a new Task that is backed by JS
- Task based ports allow for better composition and less friction when the 
backing JS is eventually implemented in Elm.

I'd love to hear what people think about this. Perhaps I'm missing some big 
issues with the idea for instance, or maybe it's an awesome idea :) What do 
you all think?

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

Reply via email to