Skip to content

Instantly share code, notes, and snippets.

@JoeyEremondi
Last active January 6, 2016 18:30
Show Gist options
  • Save JoeyEremondi/aa87d1ce86a3e5b2944c to your computer and use it in GitHub Desktop.
Save JoeyEremondi/aa87d1ce86a3e5b2944c to your computer and use it in GitHub Desktop.

Proposal: revised Elm Native format

Right now, Native doesn't play well with dead-code elmination. Additionally, Native review is a big bottleneck for producing and publishing new librairies. Finally, there is no way to unclude JS code which calls ports using the Elm compiler.

Example file

/**
--The top of the file declares the structure of the Native module
--and must be a valid Elm syntax declaration
module Native.SomeModule where

--Used Native modules must be explicitly imported
import Native.Utils

--We can import pre-existing library functions
--this includes them in the final output
--but they can't be explicitly called from Elm.
--A Native module must act as an interface.
import raw "Bootstrap.js"

-every used non-Native function must be explicitly imported
import Signal exposing (constant)

--Each exposed Native function is declared as a String literal in Elm
--with the name of the JS function that defines it
--This lets us statically check that no top-level functions are missing

nativeFun = "nativeFun"


otherFun = "otherFun"
**/

//The rest of the JS file is just a series of JS statements

var nativeFun = function(x)
{
  if (Elm.Utils.constructor(x) == "Nothing" )
  {
    Elm.Utils.construct("Nothing");
  }
  //We construct "type" (algebraic data) values using a library function
  return Elm.Utils.construct("Just", 3);
};

var otherFun = function(y)
{
  return nonExposedFunction(y);
};

//We are allowed to declare other functions
//that can't be imported by Elm
var nonExposedFunction(y)
{
  var ret = [];
  //We build records out of arrays
  //and access fields using a library function
  ret["field"] = Elm.Utils.field(y, "elmField");
  ret["field2] = 3;
  
  //a library function constructs the record out of the array
  return Elm.Signal.constant(Elm.Utils.record(ret));
};

What's different in this representation:

  • The structure of the module is declared at the top, using normal Elm syntax
  • The user doesn't write a .make() function, or return a .values object. That's all done mechanically, we would be guaranteed those functions/values would always be well formed.
  • The imported Native modules, and imported and exported Elm functions, must be explicitly declared
  • Elm ADT and Record values are not manipulated directly, but using a library function, so Native modules would not break if Elm changed its internal representations.
  • There's new syntax for including a JS file in compilation, such as pre-existing libraries.

Pros:

  • The module structure is defined using Elm syntax, so we can use existing parsers, and it flows nicely with the rest of the project.
  • Much less Native review needed to ensure a module is well formed.
  • More resistant to breaking changes.
  • The import raw syntax can be used to include the JS side of Ports code.

Cons:

  • Could be slower, since we're calling library functions to access Elm values. (Could be solved by inlining).
  • Requires pre-processing of comments (could be solved by putting the structure Elm declaration in its own file).
  • There are now 3 types of JS ffi (Native, Ports, import raw)
@rgrempel
Copy link

Here's another advantage of my revision.

Since the Javascript references to Elm things would all be via the @{} syntax, and since the compiler would have to parse the Javascript, there actually would be some opportunities for the compiler to type-check the Javascript itself. For instance, in the revised nonExposedFunction, the compiler, when parsing the Javascript, would know the type of y. It would also know the type signature for Native.Utils.field. So, in principle, a certain amount of type-checking would be possible.

Now, I'm not saying that it would be complete -- it would probably not be. And, some type errors detected in the Javascript code might have to be warnings instead of errors, since it might be part of the point of the native code to do some things that the compiler can't verify in terms of types. However, in principle it is potentially an advantage if the compiler can make some explicit connections between the Javascript code and the Elm types.

@rgrempel
Copy link

rgrempel commented Jan 6, 2016

Here's another idea for native code -- it's a new idea, not related to my previous comments.

I've been experimenting with Purescript a bit, and there's an idiom they use with "native" code that might be helpful.

To back up, Elm native code currently can have its own dependencies on other Elm modules. So, there is a kind of subterranean dependency graph in the native code, which the Elm compiler (currently) doesn't know about. And, that's one of the reasons for the difficulty with dead code elimination etc. So, one of the purposes of the proposal above is to "surface" that dependency graph.

Purescript approaches this differently. Basically, native code doesn't set up its own dependencies. Instead, if a native function needs something from elsewhere, it must be supplied (from the caller) as a parameter.

Consider a native function which wants to return a Maybe. In Elm, currently, the native module would reach out and get the Just and Nothing constructors itself. In Purescript, the function would ask for the constructors as parameters (possibly a record-type parameter, if there are a number of things that need to be supplied).

The nice thing about this is that the native function is then "pure" (in a sense) -- it only relies on its parameters and whatever is natively available in the Javascript runtime. Thus, the compiler doesn't need to special-case the reasoning about what the native function is using -- it knows in the regular way, since it sees the parameters.

So, that's just grist for the mill -- it seems elegant to me, but I understand that there are a lot of considerations involved here which may lead to other approaches.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment