Skip to content

Algebraic Effects #1449

@njlr

Description

@njlr

I propose we ...

Add an Algebraic Effects system.

This is not a precise definition, just a sketch of what it might look like...

Define effects with an input type and an output type using a new effect keyword:

effect ConsoleRead of unit -> string 

effect ConsoleWrite of string -> unit 

Functions can perform effects with a new perform keyword:

let greet () =
  perform (ConsoleWrite "What is your name?")
  let name = perform (ConsoleRead ())
  perform (ConsoleWrite $"Hello %s{name}")

Functions are annotated with the set of effects they might perform during execution:

val greet : unit ->{ConsoleRead|ConsoleWrite} unit

And a normal function is equivalent to the empty set:

// String.length

val length : string -> int
val length : string ->{} int

Effect types propagate across function calls automatically:

let greetN (n : int) =
  for _ = 1 to n do
    greet ()
val greetN : int ->{ConsoleRead|ConsoleWrite} unit

And the effects of a function is the union of what it calls:

let read () =
  perform (ConsoleRead ())

let write (x : string) =
  perform (ConsoleWrite x)

let readAndWrite () =
  write (read ())
val read : unit ->{ConsoleRead} string

val write : string ->{ConsoleWrite} unit

val readAndWrite : unit ->{ConsoleRead|ConsoleWrite} unit

Effects can be given a concrete handler with a new handle keyword, thus removing the effect type from the annotation:

open System

[<EntryPoint>]
let main _ = 
  handle 
    greet ()
  with
  | (ConsoleRead ()) k ->
    let v = Console.ReadLine()
    continue k v
  | (ConsoleWrite v) k ->
    Console.WriteLine(v)
    continue k ()

  0

The compiler guarantees that the entry point has no effects. In other words, all effectful computations must be a concrete handler.

Different handlers can be given, depending on the scenario:

open Expecto

test "greet works as expected" {
  let output = ResizeArray<string>()

  handle 
    greet ()
  with
  | (ConsoleRead ()) k ->
    continue k "John Doe"
  | (ConsoleWrite v) k ->
    output.Add(v)
    continue k ()

  Expect.equal (Seq.toList output) [ "Hello John Doe" ] ""
}

A simulation of this feature might be possible with computation expressions and polymorphic variants.

The existing way of approaching this problem in F# is ...

  • Impure code - convenient to write, but effects are unmanaged
  • Monadic code - effect are managed, but code is inconvenient to write

Pros and Cons

The advantages of making this adjustment to F# are ...

  • Convenience and approachability of impure code - "just write the thing"
  • Encoding of effect information in the type-system
  • Ability to write mock effect handlers for testing purposes
  • Performance improvements over monadic and DI approaches, with appropriate inlining of handlers

The disadvantages of making this adjustment to F# are ...

  • Language complexity
  • Potential for increased indirection in library and application code

Extra information

Estimated cost (XS, S, M, L, XL, XXL): XXL

Related suggestions:

Effect propagation is related to polymorphic variants. Effect annotations relate to function purity analysis.

Affidavit (please submit!)

Please tick these items by placing a cross in the box:

  • This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
  • This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions