inko-lang / inko

A language for building concurrent software with confidence
http://inko-lang.org/
Mozilla Public License 2.0
899 stars 41 forks source link

Consider re-introducing pattern matching in `let` through syntax sugar #558

Open yorickpeterse opened 1 year ago

yorickpeterse commented 1 year ago

Description

At some point Inko supported pattern matching in let, meaning you could write e.g. let Some(val) = some_option else return or let (a, b) = c. This was removed due to the complexity of the implementation, as it wasn't able to reuse the same compilation steps for match expressions, instead having to re-implement/copy portions of it.

I would like to re-introduce this in a limited form, as not having it can result in deeply nested code. For example, now you end up with this:

@state.update_with(updates) fn move (rooms, updates) {
  updates.into_iter.each fn (entry) {
    match entry {
      case { @key = name, @value = status } -> {
        let room = rooms.get_mut(name)
        let update = match room.status {
          case Humid or Button -> false
          case old -> {
            status >= old or room.last_update.elapsed.to_nanos >= wait
          }
        }

        if update { room.update(status) }
      }
    }
  }
}

Instead of something like this (which is nicer):

@state.update_with(updates) fn move (rooms, updates) {
  updates.into_iter.each fn (entry) {
    let { @key = name, @value = status } = entry
    let room = rooms.get_mut(name)
    let update = match room.status {
      case Humid or Button -> false
      case old -> {
        status >= old or room.last_update.elapsed.to_nanos >= wait
      }
    }

    if update { room.update(status) }
  }
}

As for the implementation: we should implement this as a desugaring pass that operates on HIR, before type-checking. Essentially for every let PATTERN = VALUE we encounter, we turn that into match VALUE { case PATTERN -> after } where after is all the code that follows the let expression. Thus this:

let (a, b) = c
foo

Becomes this:

match c {
  case (a, b) -> {
    foo
  }
}

Re-introducing let ... else would complicate matters, as the expression of else must return/break from the surrounding scope, as the code that comes after can't run without the pattern matching. Part of the complexity of the past implementation involved handling that, generating the correct code, type-checking, etc. As such I'm inclined to just not re-introducing that, and directing people to just using match for such needs. Given the two most common cases are destructuring tuples and classes (and not e.g. enums), I think not supporting else should be fine.

With that all said, I find myself mostly needing this when iterating over a Map and wanting to break the Entry values down into separate variables. Outside of that I haven't had a real need for this, so I'm not yet convinced this is actually worth the maintenance overhead and compiler complexity.

Related work

No response

yorickpeterse commented 1 year ago

Another option is to solve this at the standard library. Types such as Entry, Option, Result, etc could provide a let method that yields in the OK/some/etc case, passing its components as separate arguments. For example, for Entry you'd then write something like this:

map.into_iter.each fn (entry) {
  entry.let fn (key, value) {
    ...
  }
}

You'd still have an extra level of indentation, though at least we wouldn't have to further complicate the compiler.

yorickpeterse commented 1 year ago

Kotlin has a somewhat similar feature to the let method described above: https://kotlinlang.org/docs/scope-functions.html#let