With native modules going away in 0.19, I wanted to ask the community if there were any known use cases that native modules addressed that simply cannot be accomplished using either ports or custom elements? From what I know of ports (which I have used) and custom elements (which I haven’t) it seems like there is a viable approach to everything… is that true?
Turing completeness probably means that little or nothing can’t be done. It’s just that some things may be more difficult.
Ports are a great fit if you really just need to implement some fire-and-forget functionality or have global streams of information you need back.
On the other hand, ports can be quite awkward if you need responses back from commands because you won’t get them and instead must subscribe to a stream of responses. This isn’t that hard for a tightly specified connection where you want to send from only one place in your code and get results back at only place. However, consider doing something like using Phoenix push messages as an alternative to HTTP for an API. In that case, you would probably want the same sort of code structure choices that Elm affords to HTTP. Imagine an implementation of HTTP support in Elm in which to get a response to an HTTP request you had to subscribe to the single stream of all responses to all HTTP requests. What would this do to your code? Can you work around this and use ports? Yes, but you will need to do a lot of work to do so including possibly using something other than or in addition to Elm’s command mechanism.
Finally, one of the recommended ways to use ports for complex interop in Elm is to think of them as a bridge to another actor in an actor system. We can generate messages for that actor from multiple places within our Elm code but the actor sends back its entire public state over the subscription rather than responding to the individual messages. This is a pretty good conceptual model for a lot of things. For example, you could put your data store on the other side of such an interface. If your needs fit this pattern, ports are potentially a good fit. The chief downside is that if the state coming back is large, it will all run through the decoder when it comes back. What’s more, a naive implementation won’t be able to share data between updates which will put more pressure on memory and the GC and undermine Html.Lazy. An incrementally decoded change feed would address these issues but potentially complicates the structure on each side unless you can readily make things look like a sync feed. But if memory pressure and decode time isn’t a concern and your needs can fit this “actor” model, then ports are a great fit.
So, the correct question to ask here in deciding how to proceed is probably less about what can’t be done but about what things will be too difficult or inflict too much structural damage on your code if they don’t fit with the patterns that ports are good at implementing.
I know that Phoenix actually has more to do with effects managers than native code — though until Elm regains websocket support in 0.19, it is also a native code issue — but I picked it because it’s a common use case that wouldn’t need a lot of explanation and touches on essentially the same issues.
In Elm 0.18, I’ve used both the non-effects-manager-based support for Phoenix and the effects-manager-based support. The ugly part of using the former was that we had to write explicit code to leave channels when we were no longer interested. In the latter, this was all taken care of for us at subscription computation time. One could build mechanisms to handle this in a similar manner to effects managers but this becomes another example of replicating something much like the standard Elm architecture to get around extensibility limitations in the architecture and that has sort of an icky feeling to it.
You’re likely to get comments on this that have more to do with communicating opinions about this than they have to do with providing concrete information. The line between “this code is bad” and “I don’t like the way this code looks” might be a little fuzzy, but we have to try and make that distinction on this topic.
I’m certain that folks will be able to present specific situations in which neither ports nor custom elements are palatable. However, the core team has yet to be presented with a real production use case for which we couldn’t find an acceptable solution using ports and custom elements.
That doesn’t mean it will never happen. But at this point we don’t feel that Elm’s interop approach has been invalidated in practice, we don’t expect that it will be invalidated, and we don’t believe it’s sufficiently likely that you’ll encounter something so difficult that it should steer you away from using Elm.
Lastly, now that Elm 0.19 is released we’re going to look into covering some of the most common use cases that need ports right now, like the oft-cited websockets.
There’s arguably nothing that you cannot do with ports as opposed to native modules. When you boil it down, an Elm-to-JS port is just an asynchronous function call from Elm to a JavaScript function. Conversely, a JS-to-Elm port is an asynchronous function call from JavaScript to an Elm function whose parameter value is type checked upon entry into Elm.
You can create what I will call ‘pseudo-synchronous’ behavior if your JS-to-Elm port acts as a callback of your initial Elm-to-JS port. Be warned, however, doing this blindly is considered an anti-pattern. A well designed Elm application will not rely on these JS-to-Elm callbacks being successful and will be able to recover in the event of an error on the JS side. This was the subject of the “Importance of Ports” talk given at Elm Conf in 2017.
So to reiterate my initial point, you’d really be hard pressed to find something that you cannot do using ports but that you can do using native modules. It may be unwieldy to set something like this up just to make what is otherwise a pure function call, but the vast majority of pure function behavior can be written directly in Elm. At my company, we’ve collectively written about 50k lines of Elm across 8 or so different applications (ranging from 500 to 30k lines per app), and have used ports throughout without much of an issue.
I think most of the complaints about native modules being removed are either from people who did not heed the initial warning to not use an undocumented API and need to do work to rewrite/rearchitect their 0.18 code, or people who are not particularly comfortable with Elm and are intimidated by the idea of writing ports. The latter of which is completely reasonable, as writing ports effectively requires a good understanding of model-view-update architecture, subscriptions, and decoders.
Yes. You can imagine the Native/Kernel code as being a driver to the runtime. Each kernel module providing some functionality to the runtime. Most of this functionality relates to the web platform and it can be conceived as an “actor” that can receive commands from Elm and can send back messages.
If you think of your Elm app as being an actor, it has ways to send messages to JS and ways to receive messages from JS (the ports).
So, if you need something to be executed in JS, you can simply send JS a message with what you need and you have subscriptions listen for replies. That’s all.
The only challenge is that you have to implement some kind of message routing and this can be cumbersome and error prone but… in practice, the need for this kind of functionality is quite rare and you can invest the extra-care. You pay the price once and then you seldom have to revisit that functionality.
There is another reason. In open source you learn by reading the source. Then you like what you see in the HTTP module but, oops, you can’t do it. So it seems there are two models of programming some things but you are only allowed to use one. This is not a value judgment, it’s just an observation.
That’s a reasonable way of looking at it, but it focuses on certain aspects (reading + learning from open source) while disregarding others (such as the guarantee that no package in the entire Elm ecosystem will cause a runtime exception, or that the kernel code API will continually get rewritten as new version of Elm are released, or that JavaScript may eventually not even be a compilation target).
If your point is that you should be able to replicate and modify anything you read in open source code, you absolutely can, even in Elm. Start at the compiler. Remove the checks for native modules, recompile the binary, and then write all the unsafe code you want. You will have thrown out the guarantees that Elm won’t crash at runtime, and may be stuck with incompatible native code if the compiler gets another full rewrite in a future version of Elm, but you are free to do so. This is no different than downloading the full React.js repo and changing its source code before building it. You can then work on a React-like project with whatever constraints you did not like about React removed.
In neither case are you writing code that fits into the original ecosystem, and your code is arguably no longer Elm 0.19 or React.js, but nothing is stopping you from doing so.