Hacker News new | ask | show | jobs
by helix278 748 days ago
This sounds like a weaker form of algebraic effects, but it is still cool to see such a language and see what you can do with it.
1 comments

I never understood "algebraic effects". But I understand the Hurl docs. Are "algebraic effects" basically the "toss" keyword from Hurl? If so, how are "algebraic effects" stronger than the "toss" keyword?
More general than the toss keyword even. If you're interested in concrete examples, you might have a look at the koka documentation
As mentioned in another comment here, the unwind is expensive. That is, searching back through the stack finding the original call site that triggered the exception. How do algebraic effects handle that performance hit?
You can think of algebraic effects kind of like:

From callsite Foo, I call out to a function with a known name, Bar, with one or more parameters. The runtime (or sufficiently smart compiler) searches upwards in scope, for a handler Baz that provides the function with that name. Baz's Bar is then called, with both the provided parameters, and, crucially, a function that is "the rest of Foo."

So, an implementation of Exception with effects would ignore the resume, and look something like:

    define_effect Exception {
      // only one function provided by Exception, but could have more
      throw(String)
    }
    define_handler printExceptions {
      throw(msg, resume_func): { println(msg) }
    }
    define_func getPage(url) {
      request = http.get(url)
      if not request.ok { throw("Could not download") }
      return page
    }
    // main entrypoint
    withHandlers printExceptions {
      page = getPage("https://cheese.com")
      println(page.text)
    }
But you could also write an "on error resume next" handler for Exception. Exceptions thrown with this handler would be equivalent to toss. (in real life you'd probably write a different effect, rather than re-using the Exception/throw effect):

    define_handler onErrorResumeNext {
      throw(msg, resume_func): { 
        println(msg)
        // YOLO, call it anyways
        withHandlers onErrorResumeNext {
          resume_func()
        }
      }
    }
    withHandlers onErrorResumeNext {
      page = getPage("https://cheese.com")
      println(page.text) // Probably uninitialized - a better  :P
    }
Breaking away from the Exception example, two cool examples are:

    // Stream - potentially infinite and/or asynchronous iterables
    // basically just like python generators
    define_effect Stream {
      emit(item)
    }
    define_handler toList(accumulator) {
      emit(item, resume_func): {
        if item == nil {
          return accumulator
        } else {
          accumulator.append(item)
          withHandlers toList { resume_func() }
        }
      }
    }
    define_handler take(how_many) {
      emit(item, resume_func) {
        if how_many == 0 {
          emit nil
        } else {
          withHandlers take(how_many-1) { resume_func() }
        }
      }
    }
    define_func fib_stream() {
      a, b = 0, 1
      loop {
        emit a
        a, b = b, a+b
      }
    }
    // Usage
    first_five = withHandlers toList {
      withHandlers take(5) {
        fib()
      }
    }
    println("The first five fibonacci numbers are", first_five)
And this one I'm just going to lift from the Unison documentation [1]:

    Each.toList do
      a = Each.range 0 5
      -- beginning of resume_func f_A
      b = each [1, 2, 3]
      -- beginning of resume_func f_B
      guard (a < b)
      -- beginning of resume_func f_C
      (a, b)
    -- yields
    [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
Note that this has the same semantics as:

    results = []
    for (a = 0; a < 5 ; a++) {  // for each a, call f_A(a)
      foreach b in [1, 2, 3] {  // for each b, call f_B(b)
        if not a<b { continue } // if a<b, call f_C(a, b)
        results.append((a, b))
      }
    }
And it is possible to do this, because these resumable functions can be resumed an arbitrary number of times - it's up to the handler. They also can pass parameters back to the resume_func.

Exceptions/hurl = exactly 0 resumptions toss = exactly 1 resumption effects = 0..many resumptions

[1]: https://share.unison-lang.org/@unison/base/code/releases/3.5...

I see. Many thanks for this! Sounds a bit like a mixture of coroutines and dynamic scoping. Very interesting!