Very well put Mark. Regarding Nicholas' advice on "The process"
> 6. Do things in life that make you happy. If it upsets you that Elm lacks something you think is super important, maybe take a break and come back later. That's what I've done. I'm back in JS land and Getting Things Done. Elm's lack of a simple mechanism for interaction with external APIs (other than pub/sub style APIs, which it does well, but they are the minority) limits its practical application. The proposition that the tiny core Elm team will deliver built-in integration to all APIs in every JS runtime context (Electron? Cordova? Lambda? Chrome Extension? React Native? Arango? Espruino? ...) is clearly not realistic. On Thursday, April 20, 2017 at 4:07:47 AM UTC+10, Mark Hamburg wrote: > > Since the call is for concrete use cases, here is one: Reading from local > storage. > > My program wants to cache various things in local storage or for the > purposes of this example it wants to read from various locations in local > storage to get the values needed at various points in the model/UX. If we > didn't use local storage and just used HTTP, we would generate HTTP > requests at the points where we needed the information and using Elm > message routing get the results delivered back. We would like to do the > same thing with local storage as a cache — possibly followed by an HTTP > fallback if we found nothing in local storage but that's beyond the scope > here. > > The analogous API to the HTTP case would be something like: > > get : (Result Error Json.Decode.Value -> msg) -> String -> Cmd msg > > > Or to make it more composable — cough, tasks v commands, cough — we might > have: > > getTask : String -> Task Error Json.Decode.Value > > > Given that the Elm local storage effects manager seems to be on indefinite > hold, we need to use ports. So, what does it take to do this using ports? > > Command ports don't return results. Subscription ports don't convey > information to the JavaScript side. So, we have to use a pair of them. A > command to trigger work on the JavaScript side and a subscription to > receive values back. But how do we match results coming back with requests > coming in? How do we tag those results appropriately for delivery? > > There would seem to be two options: > > 1. For every request we need to make, create a separate pair of ports. > Now, we don't need to pass in the key string because the key is built into > the port identity. In fact, it would be bad to pass in the key string since > that would reintroduce the question of which response goes with which > request. This approach will work if we can statically enumerate ahead of > time all of the keys we are interested in AND we are prepared to write > JavaScript handler code for each and every request. This might be viable > for the big flat applications that some people like to advocate but it's a > pretty messy maintenance situation and it breaks the moment we can't > statically enumerate the keys of interest. > > 2. When we send a request in, we tag it with an identifying number and > maintain a dictionary mapping those id's to tagger functions. The > JavaScript side includes the id in its response — basically the same thing > Phoenix push messages do — and the subscription port feeds into logic that > looks up the id and tags and delivers the result. Those of you who are > horrified at the thought of storing functions in the model should perhaps > stop right here because that id to tagger function dictionary is doing > exactly that. But even if one gets beyond that, note that this approach > won't work just like HTTP because we will need at some point before this > becomes a command to update the delivery map. That can either happen by > replacing commands with requests or by allowing arbitrary update code to > touch the shared global id generator and delivery map. Neither approach is > as straightforward as the HTTP case. > > This could be addressed by extending command ports to allow them to return > responses but if we're doing that, why not just make them composable and > generate tasks that take a Json.Encode.Value as input and return a Task > PortError Json.Decode.Value (where PortError might just be another > Json.Decode.Value or it might be some other response structure that could > reflect other implementation errors TBD): > > port get : Json.Encode.Value -> Task Task.PortError Json.Decode.Value > > > Now, I can wrap this up in code to do the encoding of strings to JSON > (trivial) and map the resulting task result to do any appropriate decoding > and delivery. > > One could argue that this would all be handled in the local storage effect > manager thereby rendering this example moot. But as noted above, that > effects manager seems to be on indefinite hold. Furthermore, local storage > was just a convenient and simple example. The exact same arguments could > apply to almost any storage mechanism we wanted to talk to. If the Elm > community were cranking out effects managers to handle these cases that > would clearly mitigate this issue but that hasn't been happening. What's > more such effects managers would almost certainly end up writing kernel > (née native) code to provide task support for the interaction with > JavaScript so instead of having a single mechanism for marshaling these > interactions and keeping the relevant code out of the kernel, we would > instead have a growing number of pieces of code wanting kernel rights. > > Mark > > On Fri, Apr 14, 2017 at 11:39 AM, Simon <[email protected] <javascript:>> > wrote: > >> +1 for Task ports. >> >> On Friday, 14 April 2017 20:32:24 UTC+2, Nicholas Hollon wrote: >>> >>> The process: >>> >>> 1. Share your ideas, experience, & code on elm-discuss. >>> 2. Accept that you have no direct influence over what Evan works on next. >>> 3. Look at the release history <http://elm-lang.org/blog> for Elm. >>> Notice that changes happen slowly. Notice that improvements to the >>> JavaScript interface (ports, tasks, subscriptions) are spaced out by at >>> least a year. Notice that 0.17 has only been out for about a year. >>> 4. Remember that things are going to get better! Just because something >>> isn't being worked on right this minute doesn't mean that it isn't going to >>> be improved in the future. >>> 6. Do things in life that make you happy. If it upsets you that Elm >>> lacks something you think is super important, maybe take a break and come >>> back later. >>> >>> >>> >>> On Friday, April 14, 2017 at 10:24:46 AM UTC-7, Conner Ruhl wrote: >>>> >>>> What is the process for requesting these sort of features? Is there one? >>>> >>>> On Saturday, August 13, 2016 at 10:31:07 AM UTC-5, James Wilson wrote: >>>>> >>>>> 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] <javascript:>. >> For more options, visit https://groups.google.com/d/optout. >> > > -- 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.
