Elm-Functions on node for complex domain

Hello!

I’m investigating if my approach using elm with node on the server is a good idea or not.

I’m currently using elm on the front end and I’m very happy with the results so far. And I think elm would be a great fit for representing the complexity of the domain for my current project.

  • it should solve a complex business logic problem
  • data is used only from the input from the Run Value port
  • no Model needed
  • the app would run without any side effects (except the output)
  • performance is a secondary concern, the Run function is called not very often and isn’t very memory or cpu intensive

I know elm is designed to work in the browser, and I’m aware of all the discussions about it, I’m also aware of purescript, reasonml, typescript aso. Still - I would love to use elm.

Has anyone used elm this way? See code below. Any caveats? Feedback is much appreciated!

EDIT: Updated version after feedback:

Previous version:

Main.elm

port module Main exposing (main)

import Json.Decode as D exposing (Value)
import Platform exposing (worker)


type Msg
    = Run Value


main : Program () () Msg
main =
    worker
        { init = always ( (), Cmd.none )
        , update = update
        , subscriptions = always (Sub.batch [ run Run ])
        }


update : Msg -> () -> ( (), Cmd Msg )
update msg _ =
    case msg of
        Run value ->
            case D.decodeValue (D.list D.string) value of
                Err _ ->
                    ( ()
                    , Nothing |> output
                    )

                Ok items ->
                    ( ()
                    -- very complex stuff happens in the real app here
                    , items
                        |> List.filter (String.length >> (<) 4)
                        |> List.length
                        |> Just
                        |> output
                    )


port run : (Value -> msg) -> Sub msg


port output : Maybe Int -> Cmd msg

run.js
const { Elm } = require("./elm-main");

const App = Elm.Main.init({});

const exec = async (outputCallback, runCallback, input) => {
  const p = new Promise(resolve => {
    outputCallback.subscribe(resolve);
  });
  runCallback.send(input);
  return p;
};

(async () => {
  const result = await exec(App.ports.output, App.ports.run, [
    "mouse",
    "cat",
    "dog",
    "cow"
  ]);

  console.log("result", result);
})();
3 Likes

The requirements of the backend are very different from the requirements of the frontend.

The main risk is that you will run into the limitations of the language, especially when trying to access functionality with side-effects (databases, files).

Also, as a little bit of context, there was an attempt to create an infrastructure for the backend back in the days of 0.18. The attempt used undocumented Kernel code and put together a series of libraries. The project ended up being abandoned when the team implementing that project failed to negotiate any way forward with the Kernel restrictions that went into effect with 0.19.

It is perfectly fine to explore this domain, play with it BUT, thinking that you will be able to have something like this in production anytime soon is hubris. If you are willing to put in the research effort, you could explore the domain and produce some kind of review of options. This article details what has worked so far in terms of process.

It’s definitely not production worthy, but I had a play around with this idea using an example from ‘Domain Modelling Made Functional’ - (the example being an ‘Order Taking’ workflow that is a pipeline of functions that take a JSON object of the order and will eventually output some JSON that describes events that happened during the workflow).

It’s using a Typescript server with graphql as the data exchange layer.

I found Elm an absolutely lovely language to model the domain in. However I had a couple of pain points with it:

  • When the workflow needs to do any sort of effect the neat pipeline of functions has to be broken up (you can kind of get away with doing http requests as Tasks but if you need to communicate with a database / filesystem that has to be done via ports).
  • You have to buy into the node ecosystem - this may be fine but I would imagine the amount of Typescript / JavaScript one would need to write as a project grows would be much larger than would be needed for a frontend Elm app

I think if I had a server application that required very complex domain logic but most of its effects were done via http I may consider it - but at that point something like PureScript / FSharp / OCaml may be more ergonomic.

Anyway here’s the repo I put together if you’re interested. Id be very interested if you explore the idea though!

1 Like

Production looks very different to different projects and people.

Calling someone’s proposal (or someone themselves) arrogant is quite an aggressive response.

I don’t think @ni-ko-o-kin is looking to create the ultimate blessed solution for using Elm in the backend.

He has some requirements, and an approach for his use case, and just wants some feedback.

Linking to that generic piece about contributing to elm core projects and proposing him do a bunch of work for no reason doesn’t seem very helpful.

My input: for the use cases you listed with the requirements you write, I don’t see any problems with your approach, so I say go for it. A couple of things to look into:

If you are going to call your JS snippet more than once it looks to me like you will be subscribing a callback to the outputCallback port each time. This could kill your server if it happens too many times.

Also keep in mind if you are going to have concurrent requests that you may have to send an id of sorts to identify call with response to avoid possible race conditions.

On the Elm side, a nice trick if you only have one message is to use a type alias instead of a custom type, and you save yourself the dummy case statement in update.

Good luck and let us know how it goes!

7 Likes

Or just use destructuring:

update : Msg -> () -> ( (), Cmd Msg )
update (Run value) _ =
    case D.decodeValue (D.list D.string) value of
2 Likes

Thank you all for your replies!

@dmy Nice idea, I never thought about using destructuring in the update function. But I will have multiple messages.

@joakin Thx for the encouraging words and the constructive input! Very helpful! I will incorporate them and post an updated version soon.

@andrewMacmurray Very nice solution. How do you ensure that no race condition happens with the placeOrder mutation? Or does graphql not allow for the same mutation request until the previous one has finished. (I’m not very familiar with graphql servers.) Or do you call Elm.Main.init(…) for every mutation request?

@pdamoc I think you misunderstood me. The elm part wont need any file access or calls to the database - no side effects at all, except the output. And I don’t want to create a whole server in elm, just a pure function to solve a very complex domain. The current implementation is about 2k loc and is pure too - but in javascript, without the safety elm has to offer.

Oops, my bad. I did misunderstood the requirements. In that case, the best option might be just to call the elm function directly. This would require you to write a small script that would patch the output of the compiler so that the function is exposed.

Look at the compiler output. The javascript file that the compiler outputs is quite readable. You could expose a function like $author$project$Main$complexFunction by just adding a line like Elm.complexFunction = $author$project$Main$complexFunction; to the output before the final } and then you would be able to use it from JS just like any other JS function (e.g. console.log(Elm.complexFunction(inputdata)) ).

One thing you need to make sure is to use that function one inside the body of the main so that it is not eliminated by the DCE. For example:

main = 
    let 
        _ = complexFunction dummyData
    in 
    text "hello" 
2 Likes

@ni-ko-o-kin thanks! I don’t actually know how the graphql server it’s set up with deals with concurrent requests, but it’s set up with Elm.Main.init(...) being called per request.

I think Elm can be an excellent choice for ‘complex logic’ - the reason being that functional languages are generally a very efficient way of writing complex code, and I mean efficient in terms of the effort and brain power of the developer. I used to write complex stuff in Java, and cannot imagine going back to that. More complex problems generally need you to spend time thinking about the domain and issues you are actually trying to work out, and not wrestle with the language you are working in.

It is really quite simple to put a little javascript wrapper around some Elm code. For example, I have recently been working on generating Elm code for stubs for AWS services - the javascript part loads the input files and writes the output files, passing them through the Elm part for the complex transformation bit, which is a real joy to write in Elm. Other functional programming languages can be a good choice too, but I also appreciate the ease with which Elm can be run within a javascript engine.

I typically structure such Elm programs as a state machine. For example, for code generation the states might represent initial state, parsing the input, transforming into intermediate data model, and generating output code, and some kind of error state for when things don’t go to plan. If there is some IO in the middle, the state machine can be held in the model in a particular state, waiting to continue once the Cmd produces an effect to indicate it has completed (calling a port, or HTTP, typically). So once I get the input into a port, the program moves from the initial state to having parsed input, for example.

2 Likes

I have a new version ready with concurrency in mind. thx to @joakin for the hint. The code is a lot more complicated than the first one. If you have any questions, please post them here! I would love to hear some feedback on this more powerful version:

@pdamoc Messing with the generated js feels too risky for me in case the compiler changes. But thx for the input!

@rupert thx for the answer! Elm is just perfect for such problems!

1 Like

I further improved the code. The nice thing is, that the example functions (F1.elm and F2.elm) are completely decoupled from the Job-Logic (Main.elm) that does all the hard stuff. And that is making implementing new functions as simple as possible. I think the experiment is over for now. I’m very happy with the result. Lets see how this works out in a production app.

3 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.