Skip to content

Transparent tasks and commands #371

@robinheghan

Description

@robinheghan

Since Gren is a pure, functional language, it lends itself well to unit and integration testing. Opaque tasks and commands, however, stands in the way of this. It's impossible to inspect, mock or otherwise override tasks and commands in a reliable way.

In Elm, the elm-program-test framework gets around this by having the user define a application specific Effect type. This type is then converted to a Cmd at the last possible time. This is a lot of work, though, and wouldn't have been necessary if tasks and commands weren't opaque to begin with.

This proposal aims to make tasks and commands transparent, to further improve the testability of Gren code.

This proposal depends on the implementation of proposal #218 (tagged values).

Re-defining Tasks as a tagged union

Tasks, especially those that perform side-effects, are often created in kernel code. However, in order to maintain purity they are never executed until it escapes application code and enters the runtime.

So why not represent them as data?

module Task exposing (Task)

import Time

type Task err ok =
    | Succeed ok
    | Fail err
    | Sequence (Array (Task err ok))
    | RetrieveCurrentTime { onSuccess : Time.Now -> ok }
    ...

Each platform package (gren-lang/browser, gren-lang/node) would then define their own Task type, which will be a superset of the core Task type.

module Task exposing (Task)

import Task as CoreTask from "gren-lang/core"
import Bytes exposing (Bytes)
import FileSystem.Permission as FS

type Task err ok =
    | ...CoreTask.Task
    | FileSystem_ReadFile
        { permission : FS.Permission
        , toError : { code : Int, message : String } -> err
        , toOk : Bytes -> ok 
        }
    | FileSystem_WriteFile
        { permission : FS.Permission
        , content : Bytes
        , toError : { code : Int, message : String } -> err
        , toOk : {} -> ok 
        }

Note that, since gren-lang/core's Task is a subset of the platform Task, it's a valid value wherever the platform type is expected.

Cmd

Cmd is really just an opaque type alias for Task Never a. One might ask if we really need a separate type for this, but answering that question is out of scope for this proposal. What will have to change, however, is the opaque nature of Cmd.

When the application's update function return a Cmd, the platform's entrypoint will need to evaluate the underlying Task in order to perform the intended side-effect.

Since the Cmd/Task is transparent, it's entirely possible for the application to define its own Task type which is a superset of the platform's Task. It's also possible for the application, or a framework that's similar to elm-program-test to perform its own evaluation of Tasks. This gives the user the flexibility to ignore, mock or transform specific side-effects.

A big benefit of this design is that the user has a much easier way of testing their applications, with minimal changes required in their existing code.

This will require a lot of work from the core team, but it might actually end up moving more code from JS to Gren, which is a big win.

Metadata

Metadata

Assignees

No one assigned

    Labels

    language proposalproposal of a major feature to the language

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions