2025-03-03

Domain modeling made functional (Part 2)

TLDR

In part 1 we took a look at functional domain modeling in general. Now we focus on a comparison of F# and Kotlin in order to apply it.

F# is a really nice language and as most of its proponents on twitter rightfully complain, it's a bit underrated. Kotlin has the same problem: Incredibly well designed language, but always fighting to eat into the host language's cake. While type definitions in Kotlin can't match that of F#, they are very close. Other things like constructor functions or asynchronous functions are better in Kotlin. Features like monad comprehensions are on par.

Type definitions

Let's start with the simple type definitions. Here in F# and here in Kotlin. F# is probably unbeatable here, there is not much more to strip from type definitions like these:

type WidgetCode = private WidgetCode of string
type GizmoCode = private GizmoCode of string
type ProductCode =
    | Widget of WidgetCode
    | Gizmo of GizmoCode

To get the equivalent Kotlin code we do:

sealed interface ProductCode
data class WidgetCode(val value: String): ProductCode
data class GizmoCode(val value: String): ProductCode

Quite close, hm? F# get's bonus points for using the term type. And some personal bonus points for inverting the place where we define which types are actually part of the ProductCode type. By just taking a look at it with a few meters distance, I would say that F# is easier to read for non-programmers than Kotlin. But it's quite close.

Here is an example video how efficient you can rewrite data class-like types from F# to Kotlin:

Complete type definitions

Those definitions are incomplete though, you can find a lot more lines of code about those types in the links above. In F# we can find the following functions:

module WidgetCode =
    let value (WidgetCode code) = code
    let create fieldName code =
        let pattern = "W\d{4}"
        ConstrainedType.createLike fieldName WidgetCode pattern code

First, the declaration of value is not necessary in Kotlin, it's already included in the val declaration in the data class. The closest possible code in Kotlin looks like:

object WidgetCode {
    fun create(fieldName: String, code: String) {
        private val pattern = Regex.fromLiteral("W\\d{4}")
        fun create(fieldName: String, code: String?) = ConstrainedType.createLike(fieldName, ::WidgetCode, pattern, code)
    }
}

But we can move it to the WidgetCode class and change it a bit

@ConsistentCopyVisibility
data class WidgetCode private constructor(override val value: String): ProductCode {
    companion object {
        private val pattern = Regex.fromLiteral("W\\d{4}")
        operator fun invoke(fieldName: String, code: String?) = ConstrainedType.createLike(fieldName, ::UsStateCode, pattern, code)
    }
}

And made three things better: 1. Data classes in Kotlin by default expose a copy method which would bypass validation, which is prevented with the private constructor (and the annotation for now). 2. We moved the factory method into the class, they belong together and should live in the same place as a coherent module. 3. Using operator function, we can still call WidgetCode("foo", "myCode") like a constructor and get a nice Result object back.

Function declarations

Well, I have to admit as someone who never used F# before, I had my trouble deciphering the lambda-oriented function declarations with type inference, especially when they were higher order functions. Let's take for example this one


type TryGetProductPrice =
    ProductCode -> Price option
    
let internal getPromotionPrices (PromotionCode promotionCode) :TryGetProductPrice =

    let halfPricePromotion : TryGetProductPrice =
        fun productCode ->
            if ProductCode.value productCode = "ONSALE" then
                Price.unsafeCreate 5M |> Some
            else
                None

    let quarterPricePromotion : TryGetProductPrice =
        fun productCode ->
            if ProductCode.value productCode = "ONSALE" then
                Price.unsafeCreate 2.5M |> Some
            else
                None

    let noPromotion : TryGetProductPrice =
        fun productCode -> None

    match promotionCode with
    | "HALF" -> halfPricePromotion
    | "QUARTER" -> quarterPricePromotion
    | _ -> noPromotion

we see the getPromotionPrices function which takes a promotionCode and returns a function which takes a productCode and returns an option of price. The fact that those two declarations are in different files with a lot of space between them made it harder to understand for me. In the function we have three local functions, each of them a possible return value. The return value is then determined by simple switching over the promotionCode.

The most similar Kotlin code I could come up with is

internal fun getPromotionPrices(promotionCode: PromotionCode): GetPromotionPrices {
    val halfPricePromotion: TryGetProductPrice = { productCode ->
        if(productCode.value == "ONSALE") {
            Price.unsafeCreate(5f)
        } else {
            null
        }
    }

    val quarterPricePromotion: TryGetProductPrice = { productCode ->
        if(productCode.value == "ONSALE") {
            Price.unsafeCreate(2.5f)
        } else {
            null
        }
    }

    val noPromotion : TryGetProductPrice = { null }

    return when(promotionCode.value) {
        "HALF" -> halfPricePromotion
        "QUARTER" -> quarterPricePromotion
        else -> noPromotion
    }
}

which I consider close to identical, no better, no worse here.

Result handling

Monads are everywhere in functional programming. Option or Result types are monads. It's very helpful when your language has support to map over them, so that you don't have to unwrap them strangely. Let's take a look at this F# code:

let toValidatedOrderLine checkProductExists (unvalidatedOrderLine:UnvalidatedOrderLine) =
    result {
        let! orderLineId =
            unvalidatedOrderLine.OrderLineId
            |> toOrderLineId
        let! productCode =
            unvalidatedOrderLine.ProductCode
            |> toProductCode checkProductExists
        let! quantity =
            unvalidatedOrderLine.Quantity
            |> toOrderQuantity productCode
        let validatedOrderLine : ValidatedOrderLine = {
            OrderLineId = orderLineId
            ProductCode = productCode
            Quantity = quantity
            }
        return validatedOrderLine
    }

the let! here is a computation expression, which is a bit of a unusual concept, but basically it depends on the context you are in, what it actually does. In the given code, we're in a result block, the let binding understands that those four functions used over there return a result object. So they can return, when an error is returned, and short-curcuit. To be honest, I didn't look up what exactly the used expressions do, but usually it's something like that.

In Kotlin, there is a built-in result type, but it isn't generic over error types (and uses exception), so it's not very helpful in most cases. But Kotlin offers three other extremely nice things: Lambdas with receivers, extensions and suspending functions. With that, it's easy for libraries to build such a functionality with seamless integration. Like this excellent, small library of a result type, that I use whenever I can in Kotlin. With that, we get code quite similar:

fun toValidatedOrderLine(checkProductExists: CheckProductCodeExists, unvalidatedOrderLine: UnvalidatedOrderLine) = binding {
    val orderLineId = toOrderLineId(unvalidatedOrderLine.orderLineId).bind()
    val productCode = toProductCode(checkProductExists, unvalidatedOrderLine.productCode).bind()
    val quantity = toOrderQuantity(productCode, unvalidatedOrderLine.quantity).bind()
    val validatedOrderLine = ValidatedOrderLine(
        orderLineId = orderLineId,
        productCode = productCode,
        quantity = quantity,
    )
    validatedOrderLine
}

Here's another video how efficient and easy F# sharp code that uses computation expressions can be converted to monad comprehensions in Kotlin:

Composition

Well. I think there are two things I could complain about the code of the book.

First thing is the overly heavy usage of anonymous functions. When possible, I prefer regular funtion declarations over lambdas that are bound to a reference. Might be personal preference though.

Second thing is, that the code is overly focused on composition. I think the book doesn't exactly make a mistake by assuming that there might be a public api and a private implementation. But in most of my projects that distinction is simply overkill. Just don't do the interface layer unless your module(s) are consumed by other projects. Just do the implementation. The distinction makes everything a bit weird. The strategy pattern creeps in everywhere - if there is only one real implementation and only that one is necessary, why not just hardcode it where it's used. Why pass it as dependency, for example here:

let placeOrderApi : PlaceOrderApi =
    fun request ->
        // following the approach in "A Complete Serialization Pipeline" in chapter 11

        // start with a string
        let orderFormJson = request.Body
        let orderForm = deserializeJson<OrderFormDto>(orderFormJson)
        // convert to domain object
        let unvalidatedOrder = orderForm |> OrderFormDto.toUnvalidatedOrder

        // setup the dependencies. See "Injecting Dependencies" in chapter 9
        let workflow =
            Implementation.placeOrder
                checkProductExists // dependency
                checkAddressExists // dependency
                getPricingFunction // dependency
                calculateShippingCost // dependency
                createOrderAcknowledgmentLetter  // dependency
                sendOrderAcknowledgment // dependency

        // now we are in the pure domain
        let asyncResult = workflow unvalidatedOrder

        // now convert from the pure domain back to a HttpResponse
        asyncResult
        |> Async.map (workflowResultToHttpReponse)

Composition is not a goal per se, but a technique to achieve other things. Like reuse of code. But what for if code is already clean and concise. Or the possibility to exchange implementations. But what for, if there is only one.

I have the impression that there are more moving parts in the code than there need to be. The more moving parts, the more variants are in your code.

While that complaint is really not that important, I at least wanted to be honest about my view after the code conversion. Following the indirections and compositions was by far the most challenging part, all in all.

What is very refreshing for me is the simple way how the whole API is assembled. No dependency injection hell, just pass in some parameters and done. That's how it should be. We can learn a lot from that.

Conclusion

Intended or not, the result of applying what the book shows results in very data-oriented code. Which is nice, because I think it's really lost wisdom, that our programs are at the end only data-transformation-pipelines. Some of Kotlin's language features are a must-have there. It's hard to live without them, once one discovered their effective usage. Like data classes and smart constructors. Result types with monad comprehensions. Sealed types. Suspending functions. Local functions. Expression functions. Nullability. And the upcoming context parameters if they ever land.

I once had the pleasure to use the shown style with the shown result library and coroutines for a rewrite of an existing project professionally and it was very enjoyable, allthough our domain was quite small. I am a bit sad, that functional domain modelling and data oriented/pipeline oriented programming doesn't get applied more often.