gren-lang / gren-lang/compiler

Processes and Parameterized Modules

Open
#119 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

meta-exploration
Dominant language
Haskell
Stars
503
Forks
29
PR merge metrics
No merged PRs in 30d

Description

This is a meta-exploration. It tries to combine other explorative issues and tell a holistic narrative. This issue in particular, looks at the following issues:

  • #65
  • #81
  • #118

Processes

In the current version of Gren, applications can define a main function which return a Program, which defines how an application is initialised and how it responds to messages.

In addition, core packages define specialised modules, called effect modules, which acts as mini-programs that usually handles stateful side-effects. Examples of this is Time and Http.

The application can send and receive messages with effect modules, usually involving Tasks, Cmds and Subs.

As mentioned, this is how it works now.

In Gren's concurrency story (#65), an idea is brought up to remove both Program and effect modules, and replace them with a Process.

Inspired by Erlang actors, a Process can be thought of as a small, isolated program, that has its own state and can send and receive messages.

To start a process, you might use the following API:

main : Task ProcessExit (ProcessId model msg)
main =
  Process.spawn
    { init = init
    , update = update
    }

Process.spawn returns a Task, which in turn returns a ProcessId when fully resolved. You can use this ProcessId to send a message to the spawned process:

Process.send processId SomeMessage

Where SomeMessage is a constructor of a custom type defined by the Process in question.

As you might have guessed from the above example, main functions are expected to return a Task that spawns a process. This means you can spawn other processes as part of your program initialisation. Such processes could be the earlier mentioned Http and Time modules.

Since you need a ProcessId in order to send a message to a process, that means you can easily limit what parts of your programs can perform which side-effects. To perform an Http request, for instance, you'll need the ProcessId to the Http process.

If we were to require initialising the most important processes at program initialisation, then it would also make sense to have some sort of Decoder api for starting process and receiving some initial state. Otherwise we could easily end up with semi-valid processes.

As an example:

main : Task ProcessExit (ProcessId model msg)
main =
  Process.succeed programInit
    |> Process.require Browser.spawn -- returns initial window size
    |> Process.require Time.spawn -- returns current time

programInit : Browser.InitialData,  -> Time.InitialData -> Task (ProcessId model msg)

A process may fail to start for several reasons. For instance, it only makes sense to have a single process for accessing LocalStorage in a browser. Opening a new LocalStorage.Process should fail. Exactly how to detect new instantiations of "singleton" processes is undecided at the moment.

As seem above, it's possible to define a Process API using records to specify the skeleton of a process. However, parameterised modules might provide some additional benefits.

Parameterized Modules

It's quite common in Gren to describe things not by their concrete type, but by what abilities they have.

For example, anything you can compare can be used as a key in a Dict. Two things not containing a function can be checked for equality. A combination of init, update, Msg and Modelcan together define a Process, etc.

However, Gren itself doesn't have a consistent way of describing such abilities.

Parametric Modules (#81) tries to imagine what Gren could look like if it borrows OCaml's idea of functors.

A Parametric Module is a module which accepts other modules as input, and returns a new module. A higher-order module, if you will.

The issue proposes to add the concept of a signature module, which only contains type signatures. It could look like this:

signature module Comparable

type alias T

compare : T -> T -> Order

To continue this example, the Dict module could then be implemented like:

module Dict exposing (..)

requires Comparable as C

type Dict C.T value
  = Node C.T value (Dict C.T value) (Dict C.T value)
  | Empty

...

To use Dict, you would import it like:

import Dict(String where { T = String.String }) as StringDict

The where syntax is there to allow for some flexibility. That said, as it would likely be quite normal to require a default type T, it would be natural for most modules to define a T alias by convention, which means the above could be shortened to:

import Dict(String) as StringDict

note: all syntax displayed is just for the purposes of explanation. The actual implementation is likely to look different

A Process could also be defined using a signature module. This means that in order to implement a Process, all you would need is to have a module where init and update is properly defined. In fact, gren make could require that any module you pass into it adheres to the Process signature, in which case a main function would be uneccessary.

If Parameterized Modules where to be implemented, other ways of achieving similar flexibility is likely to be rewritten in order to reduce the overall complexity of the language. This means that the special comparable type would be removed. Same applies to "magic" functions like ==.

For == in particular, it's likely that modules like String, Int, Float, etc. would define their own equals function.

How operators like + and - should be handled once number disappears is an open question. One could create different operators for Int and Float, like + and .+. Or one could define a Number signature module, and have the operators be aliases for functions defined in that module.

Losing == might be a bitter pill to swallow, as it can be quite handy in debugging, testing and the prototype stage of development. This is where read-only reflection comes in handy.

Read-only reflection

Read-only reflection (#118) proposes to add a function, called reflect, which returns a run-time description of a value. This can then be used to implement functions like ==, compare, toString etc.

reflect can also be used to implement custom Time Travelling Debuggers.

As the compiler mangles property names when compiled with --optimize, and because it might be tempting to lean on reflect instead of properly writing functions like equals and compare, read-only reflection might end up being reserved for non-optimised application builds.

Risks

With any feature additions, there is a risk that it needlessly complicates the language or in some other way becomes a net negative.

For Gren's concurrency story #65 , I think the biggest risk is that managing processes might end up feeling very boilerplate-y. However, the idea of using ProcessIds as a permission system is very alluring.

For Parameterized Modules #81 , I think the biggest problem is that functionality which requires many different kinds of types with a specific implementation becomes difficult. Imagine how Html.Lazy.lazy6 would have to be implemented!

The biggest concern I have with read-only reflection #118 is that people won't bother with --optimized because they don't want to write their own implementation of == or whatever, even though a handwritten implementation would always be better from a performance perspective.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the linked explorations #65, #81, and #118, then compare their proposals for Processes, parameterized modules, and reflection. No implementation files or tests are named; the issue is complete only after the language design, syntax, risks, and interactions among these features are decided.

Written by the indexing model from the issue text.

Assessment

Tech stack
haskell
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.