Skip to content

Figuring out custom DSLs #161

@CLOVIS-AI

Description

@CLOVIS-AI

First, thanks a lot for this update! The possibility to create custom DSLs is amazing. That said, I'm having some issues using them.

I have a library which has its own Either-like type, called Outcome, except the left side must implement the Failure interface. I'm trying to create a custom DSL for it so Arrow users can use it as if it was Either:

interface Failure { … }

sealed class UserError : Failure {
    object NotFound : UserError()
    //
}

sealed class Outcome<out F : Failure, out T> {
    class OutcomeFailure<F : Failure>(val failure: F) : Outcome<F, Nothing>
    class OutcomeSuccess<T>(val value: T) : Outcome<Nothing, T>
}

Declaring the DSL itself was quite easy following the documentation:

@JvmInline
value class OutcomeDSL<F : Failure>(private val raise: Raise<Outcome<F, Nothing>>) :
    Raise<Outcome<F, Nothing>> by raise {
    
    fun <T> Outcome<F, T>.bind(): T = when (this) {
        is OutcomeFailure -> raise.raise(this)
        is OutcomeSuccess -> value
    }
}

@OptIn(ExperimentalTypeInference::class)
inline fun <F : Failure, T> out(@BuilderInference block: OutcomeDsl<F>.() -> T) : Outcome<F, T> =
    recover(
        block = { OutcomeSuccess(block(OutcomeDsl(this))) },
        recover = ::identity,
    )

In simple cases, it seems to work fine, however it quickly gets stuck on variance errors:

data class User(val name: String)

fun create(user: User?) = out<UserError, User> {
    ensureNotNull(user) { UserError.NotFound } // Type mismatch, expected OutcomeFailure, found UserError.NotFound

    TODO()
}

It seemed weird to me that it was expecting the OutcomeFailure type instead (why is it a Raise<Outcome<F, Nothing>> and not a Raise<F>?), so I tried to rewrite it as follows:

@JvmInline
value class OutcomeDsl<F : Failure>(private val raise: Raise<F>) :
    Raise<F> by raise {

    fun <T> Outcome<F, T>.bind(): T = when (this) {
        is OutcomeSuccess -> value
        is OutcomeFailure -> raise.raise(failure)
    }
}

@OptIn(ExperimentalTypeInference::class)
inline fun <F : Failure, T> out(@BuilderInference block: OutcomeDsl<F>.() -> T) : Outcome<F, T> =
    recover(
        block = { OutcomeSuccess(block(OutcomeDsl(this))) },
        recover = { e: F -> OutcomeFailure(e) },
    )

Note how:

  • OutcomeDsl.raise is a Raise<F> instead of a Raise<Outcome<F, Nothing>>
  • recover's recover properly instantiates an OutcomeFailure value, thus having a symmetry between block which instantiates a successful variant and recover which instantiates a failure variant

Was there an error in the documentation, or did I completely misunderstand the given example?


I was originally going to ask this on the Slack channel, but as the message grew I thought it would be better here… Is this the right place to ask such questions?

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