POP-lang is an imaginary functional language based on Dependency injection

Good morning, dear readers, in this issue we will look at what a functional language based on Dependency Injection might look like. On the cover, I gave an example about a hipster in the Middle Ages:

MiddleAges(status, lifespan) = hipster

It would be nice, by the way, to make a survey about the hipster at the end of the article – unfortunately, I didn’t have much history at school, so I can’t even offer suitable options. So – on the one hand, this line looks like pattern matching, and on the other hand, the idea is to do MiddleAges a normal function (or several) – without any magic.

In functional languages, as you know, pattern matching does not allow such liberties: values ​​can be matched either by structure or by content. In addition, you can use a very limited set of checks – the so-called guards. And there is a simple explanation for this – why expensive calculations cannot be used: such calculations must be able to be reused.

Let’s say we have a problem:

case problem:
    PlanA(timeline, benefits):
        # actions
    PlanB(timeline, cost):
        # other actions
    _:
        # give up

Let’s say we tried to choose plan A – and found out that, in this case, it is not applicable. Along the way, we did a lot of calculations, some of which could have been used to analyze Plan B – and which are now gone. That is why complex calculations are not used in pattern matching. We will also not use them – that is, we will not touch the case construction. If anything, I’ll be using pseudocode here and thereafter – what else would you expect from an article about a fictional language? () means tuples, {} – dictionaries.

Pattern matching, therefore, remains traditional, whatever one may say – but maybe we can still change something. Read more about this, of course.

In general, my personal experience in functional programming is connected with the BEAM platform and the Erlang and Elixir languages ​​(therefore, the comments of those who write in other languages ​​are doubly interesting for me personally). Some time ago I was pleasantly surprised to stumble upon the gleam language – so much so that I even wrote an article about it. If you haven’t heard of him, glitter is a statically typed language for BEAM with a C-like syntax (that is, it is more like Scala than Haskell).

It’s funny, but the idea of ​​this post was inspired by the try statement from gleam. The funny thing is that he was deprecated in the last release – so, alas, he is living his last month. It looked like this:

fn even_number(x) {
    case x % 2 {
        0 -> Ok(x)
        1 -> Err("something odd")
    }
}

fn main(x: Int) {
    try x = even_number(x)
    // ...
}

// equivalent code
fn main(x: Int) {
    val = even_number(x)
    case val {
        Err(err) -> val
        Ok(x) -> {
            // ...
        }
    }
}

This is no longer pseudocode, but gleam syntax. Function even_number returns Ok(x)if x is even, otherwise an error. The try statement acts in such a way that, in the case of an even number, we get its value again, in the case of an odd one, the main function itself returns an error.

In other words, a concept well known to Go developers:

But back to our sheep. It was the aforementioned Go-style errors that I decided to use for our case – Dependency Injection. We will do this: we will do pattern matching of just one expression – at the same time, if the matching fails, but there are some results that may be of interest to us, they are returned to us as an error.

The functions of the try statement will be taken over by the pop statement:

pop MiddleAges(status) = hipster

pop means to spit out the error if there is one. In the case when there is no error handling block (as here), then in the parent function. If there is such a block, error matching occurs in it:

pop MiddleAges(status) = hipster:
    Err("Coffee is not known in the kingdom"):
        Err("Wrong place, try Turkey or Africa")

# They have coffee
print((hipster, status))

In this example, there is an error handling block, and the result of this block (also an error) is returned to the parent function. Since pop is followed by an enumeration of errors, it seems to be intuitively clear that errors are being spit out, and not anything else.

I think the name pop is better than try because try is usually followed by an optimistic scenario and only then error handling. And we have the opposite. In addition, the try-catch block is commonly used (including in Erlang) to handle real exceptions (and gleam, by the way, does not support them in any way).

The pop construct can also have an else block, which allows you to handle successes, not just errors, and in general, is more in line with the functional style:

result = pop MiddleAges(status) = hipster:
    err:
        err
    else:
        print((hipster, status))
        Ok(status)

In this example, result is Ok() or Err(). Similar to try, pop can also be used with functions that return Ok() or Err():

val = pop my_risky_function()

Now let’s move on to the next part. The reader may be wondering how to actually implement Dependency Injection – that is, how to do MiddleAges function. This is easy to do. Let’s imagine a module like this:

# middle_ages.pop

fn match(hipster):
    True

fn status(hipster):
    ...
    some_status
    
fn lifespan(hipster):
    ...
    days

Let this module be responsible for the design MiddleAges. Function match makes a general match first – determines if a hipster is allowed at all in the Middle Ages. In our case, we see that it is possible (True). At the same time, all functions (except match) take the same input – hipster.

You might argue that functions can depend on each other’s results, or have common dependencies. And that in the above example, they can duplicate each other’s work. This is true, but this is also easy to solve: you can pass one more parameter to the functions – the context in which they will save the useful results of their work:

# middle_ages.pop

fn match(hipster):
    (True, {})

fn status((hipster, ctx)):
    ...
    new_ctx = probably_change(ctx)
    (some_status, new_ctx)
    
fn lifespan((hipster, ctx)):
    ...
    new_ctx = probably_change(ctx)
    (days, new_ctx)

Each function thus returns 2 values ​​- its own and the argument to be passed to the next function (context).

I don’t want to dwell on it too much – just trust that it’s doable and relatively easy. To make sure that this method does not contain special magic, it is necessary that the results of “pattern matching” can be found out by a simple function call:

(status, lifespan) = resolve(hipster, match_fn, (status_fn, lifespan_fn))

Each of the functions match_fn, status_fn, lifespan_fn – can return both an error and a value. Errors can be matched with the pop operator – you’ve already seen how.

This is about Dependency Injection. But, as they say, we have only done the first two steps – let’s now draw the rest of the owl. Such custom pattern matching can be built into the type system quite easily.

This is how, for example, you can specify the types of parameters in a function:

fn my_function(MyType() = val, Int() = num, x):
    ...

Moreover, MyType can be defined as a structural type – just like gleam does:

type Cat {
  Cat(name: String, cuteness: Int, age: Int)
}

The concept of structural types is simple and clear, and there is no point in abandoning it. In addition, the type can use pattern matching a la Dependency Injection:

fn my_handler(JwtAuth(user, permissions, info) = request):
    ...

In general, if our matchers perform the function of types, then they should be named as types – something like Request(), in this case. But, given that both JwtAuth and the MiddleAges discussed earlier are, after all, abstract entities, this is probably not so critical for them.

Such a notation allows you to use both type annotation and structural value matching at the same time. gleam can’t do that, besides, you can’t declare several functions under the same name in it, as it is possible in erlang. As in the example below:

fn my_fun(MyMap() = {height, width}):
    ...
    
fn my_fun("description"):
    "description here"

Types should not take part in matching the desired function when called – if there are several of them declared. A function must match that would match without an annotation with types. Perhaps some function will match, and then Dependency Injection calculations will lead to an error. Well – in this case, let’s return the error to the parent function.

That, in principle, is everything I wanted to tell you about how you can build a functional language around Dependency Injection and how it can be related to types.

At the end, according to tradition, a small survey. Only, a big request: vote not for the name of the idea (POP), but for its content! The name – I already know that it is successful: there are associations with the church, and with the stage, and even, excuse me, with the fifth point. As for the content – I’m thinking, maybe in the hubs for front-end development it can also be published in the hubs dedicated to Java and Kotlin? There fans of Dependency Injection – not just every second, but every second and third, at least. Okay, they’ll find it.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *