fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Algebraic Effects

Open njlr opened this issue 2 months ago • 7 comments

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.

  • https://github.com/fsharp/fslang-suggestions/issues/40
  • https://github.com/fsharp/fslang-suggestions/issues/201
  • https://github.com/fsharp/fslang-suggestions/issues/538
  • https://github.com/fsharp/fslang-suggestions/issues/728

Affidavit (please submit!)

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

  • [x] This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
  • [x] 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
  • [x] 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
  • [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate

Please tick all that apply:

  • [x] This is not a breaking change to the F# language design
  • [x] 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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

njlr avatar Oct 15 '25 13:10 njlr