Is there a way to dedent this happy path? ```maybe...
# getting-started
m
Is there a way to dedent this happy path?
Copy code
maybeX?.let { x ->
    f(x)?.let { y ->
        g(x, y)?.let { z ->
            h(x, y, z)
        }
    }
}
    ?: fallback
I’d like something that’s similar to
Copy code
val x <- maybeX
val y <- f(x)
val z <- g(x, y)
h(x, y, z)
y
Using Arrow:
Copy code
import arrow.core.nullable

nullable { // this : Raise<T?>
  val x = maybeX.bind()
  val y = f(x).bind()
  val z = g(x, y).bind()
  h(x, y, z)
} ?: fallback
or in vanilla Kotlin:
Copy code
run {
  val x = maybeX ?: return@run null
  val y = f(x) ?: return@run null
  val z = g(x, y) ?: return@run null
  h(x, y, z)
} ?: fallback
If you're familiar with monads,
?
kinda sorta forms a monad (but not exactly because it's not a discriminated union, i.e.
Foo??
is the same type as
Foo?
). Arrow provides these "raise builders" that act as monadic comprehensions. As you can see,
nullable
acts like a
run<T?>
block, but it gives you a
Raise<T?>
that acts like a
return@run
wrapped in a call.
➕ 1
m
@Youssef Shoaib [MOD] If I don’t want the dependency, is there a way to limit this `try`–`catch` to exceptions thrown directly in the
try
and exclude ones that have bubbled up?
Copy code
return try {
    val nestedObject = context!!
        .takeIf { callableId.callableName == COMPANION_METHOD_TO_GENERATE }!!
        .owner
    val outerClass = nestedObject
        .let(this::outerClassIfApplicable)!!

    listOf((FirNamedFunction::symbol)(this.createMemberFunction(
        owner = nestedObject,
        name = COMPANION_METHOD_TO_GENERATE,
        returnType = outerClass.constructType(),
        key = KEY,
    ) {
        val functionBuilder = this
        functionBuilder.status {
            val statusBuilder = this
            statusBuilder.isOperator = true
        }
        @OptIn(
            SymbolInternals::class,
            DirectDeclarationsAccess::class,
        )
        outerClass.fir
            .declarations
            .filterIsInstance<FirConstructor>()
            .firstOrNull(FirConstructor::isPrimary)!!
            .valueParameters
            .forEach { parameter ->
                functionBuilder.valueParameter(
                    name = parameter.name,
                    type = parameter.returnTypeRef.coneType,
                )
            }
    }))
} catch (_: NullPointerException) {
    emptyList()
}
c
There's not, and it's a really bad idea to use exceptions for control flow like that
m
@CLOVIS How does
arrow.core.nullable
work?
c
Also exceptions, but it does a lot of stuff to ensure that the exception cannot escape up the stack, and it cannot catch unrelated exceptions, and avoid the performance issues (the internal exception doesn't have a stacktrace)
➕ 1
Note that for this kind of usage you can use Arrow as an
implementation
dependency so it won't affect other modules
y
I've reached out for Arrow many times when writing a compiler plugin because yeah you end up with code like this. I really would suggest just depending on Arrow (disclaimer: I'm a maintainer). It's very stable and rather small in the grand scheme of things. Under the hood, Arrow does use exceptions, but in a smart way. It tags the exception with a unique token so it knows that it's meant to catch it. Here's an (untested) adhoc implementation of it:
Copy code
class ControlException(val raise: Raise): Exception()

class Raise {
  fun raise(): Nothing = throw ControlException(this)
  fun <T: Any> T?.bind(): T = this ?: raise()
}

inline fun <T> nullable(block: Raise.() -> T): T? = with(Raise()) {
  try { block() } catch(e: ControlException) { if (e.raise === this) null else throw e }
}
This makes
Raise
have "static" or "lexical" semantics, i.e. you can look at a
raise
call and know exactly where it'll be caught (unless someone is indiscriminately swallowing Exceptions). The implementation in Arrow is safer (detects "late"
raise
calls, extends
CancellationException
for coroutines compatibility), more performant (it turns the stack trace off by default), and has a lot of QoL features (too many to name)
m
Thank you for putting the library on my radar! I’m not obsessive about keeping a low count of dependencies but I also want to wait until I need a couple more features from it to depend on it, just to make sure every dependency pulls its weight