This is the first chapter on implementation details of Я.
Forgetting details
Probably you have heard of side effects - usually it refers to some things that running things on the background you are not able to control directly. However, effect is a very abstract term - and everybody uses it with their own semantics you should explore individually.
Я is not an exception. From now on, you can think on effects as parameterized types with an ability on focusing on their parameters - which gives us ability to forget about details.
Mapping flat morphisms
As an example, imagine we have such an expression:
expression: Optional Integer
In case of Optional, we can covariantly map a function so we don’t care if Integer
value even exists!
Some 0 `yo` is `hu` 1 ===> Some 1
None _ `yo` is `hu` 1 ===> None _
Mapping flat morphisms is easy. Let’s take good old Lists as an example - which is effect as well since we can pretend that we don’t know that there are actually many values and not the only one. You can consider it as uncertainty effect - there could be no values, one or many of them.
[1,2,3,4,5] `yo` (+ 4) ===> [5,6,7,8,9]
We don’t even care in which order we proceed values - result is going to be the same. However, it’s not always the case.
Mapping source Kleisli morphisms
With Kleisli morphisms there are often a variety of possible behaviour of the same effect. To distinguish them we use labels. Here is another example with Lists and yok operator:
[[1,2,3],[4,5,6],[7,8,9]] `yok` Plane ===> [1,2,3,4,5,6,7,8,9]
[[1,2,3],[4,5,6],[7,8,9]] `yok` Whirl ===> [1,2,3,4,5,6,7,8,9]
[[1,2,3],[],[4,5,6],[7,8,9]] `yok` Plane ===> [1,2,3,4,5,6,7,8,9]
[[1,2,3],[],[4,5,6],[7,8,9]] `yok` Whirl ===> []
As you can see, both labels let you flatten your inner List into outer one, but Whirl
label let you terminate this computation earlier if there is an empty List.
So right hand side of yok operator should look like this:
from a (l `L` tt `T'I` o)
In vanilla Haskell they do it the same with newtype wrappers - but in my opinion it’s pretty inconvenient.
Another use case of labels is State effect:
intro Unit `yuk` New `ha` State `ha` Event `ha` pop `he'he'hv__` [1,2,3,4] ===> Some 1 `lu` [2,3,4]
intro Unit `yuk` Old `ha` State `ha` Event `ha` pop `he'he'hv__` [1,2,3,4] ===> Some 1 `lu` [1,2,3,4]
As you can see on these examples, Old
label uses current state but doesn’t update it - so that instead of actual stateful operations you use viewing ones.
Let’s do some real World stuff - it’s all about input and output (that why it’s called IO). As soon as we got a character from user - we print it:
input `yok` Await `ha` output
Let’s say we are expecting only digits (an user is supposed to choose an option from a given list):
parse = on @Glyph `ho'ho` on @Digit `ho` row
And we want to retry input action until we get a digit:
input `yok` Retry `ha` Perhaps `ha` parse
It’s actually one of my favourite usage of Maybe binding into World.
Mapping target Kleisli morphisms
So when should we use labels and when we shouldn’t? There is a rule wired up in Я constraint system - we provide label per each Kleisli morphism. To count Kleisli morphisms we don’t even need to go to operators definitions it’s enough to count visual hooks:
0: `yo`
1: `yok`
2: `yokl`
However, I strongly encourage you to check these pages in order to perceive its abstract nature rather than rely on limited amount of applied use cases: yo, yok, yokl.
So, how do we compose these 2 labels for yokl operator? Just as we do with functors - using composition.
[1,2,3,4,5] `yokl` Forth `ha` Await `ha` print ===> 12345
[1,2,3,4,5] `yokl` Prior `ha` Await `ha` print ===> 54321
First label is for outer functor, the second one - for internal one. So right hand side for yokl should look like this:
from a (l `L` ll `L` tt `T'I` o)
Mapping Monoidal coherence maps
Labels are also used in Monoidal Functors as well - and they are always applied to a right hand side.
Let’s say we have two lists and we want to somehow combine these two lists… But how? One of the obvious approach is to align them according to their position in the structure:
[1,2,3] `lu'yp` Align [4,5] =====> [1 `lu` 4, 2 `lu` 5]
[1,2,3] `lu'yp` Align [4,5,6] ===> [1 `lu` 4, 2 `lu` 5, 3 `lu` 6]
Another way to combine elements is to form a cartesian product:
[1,2,3] `lu'yp` Cross [4,5] =====> [1 `lu` 4, 1 `lu` 5, 2 `lu` 4, 2 `lu` 5, 3 `lu` 4, 3 `lu` 5]
What are defaults?
I like the idea of using labels instead of memorizing default instance behaviour for each type (as they do it in vanilla Haskell). However, giving a set of label for each effect (there could be many of them if you count jointed effects as well) could be tiresome.
That’s why it’s would be nice to have default label for all effects - it’s called Run
.
[1,2,3,4,5] `yokl` Run `ha` Run `ha` print ===> 12345
[1,2,3] `lu'yp` Run [4,5] =====> [1 `lu` 4, 2 `lu` 5]
It’s possible since all labels in Я are unified. Label itself is just a special newtype wrapper:
newtype L l t i = Labeled (t i)
Labels are defined as pattern synonyms:
pattern New e = Labeled @Void @(State _) e
pattern Old e = Labeled @(Void `P` Void) @(State _) e
So that we start defining labels from single Void and gradually add more if we want to add another behaviour for the same effect.
There is still some ongoing work to constraint their usage in some cases, but nevertheless design in common is not supposed to change drastically.
Mixing labels altogether
Since we have labels it prevents Я from looking like APL. However, in some cases you have to put attention on their arrangements.
If you use yok and yp altogether, put labels on both sides:
.........................
`yok_` New `ha` State `ha` Event `ha` ...
`lo'yp` New `ha` State `ha` Event `ha` ...
In case of having more Kleisli morphisms - just add more labels accordingly. Let’s take a list of lists as an example
x = [[1,2,3],[],[4,5,6],[7,8,9]] `yi` is @(List `T'I` List Integer)
Since it’s a structure wrapped into another structure, we can traverse it in 4 distinguish ways:
x `yokl'yokl` Forth `ha` Forth `ha` Await `ha` print ===> 123456789
x `yokl'yokl` Forth `ha` Prior `ha` Await `ha` print ===> 321654987
x `yokl'yokl` Prior `ha` Forth `ha` Await `ha` print ===> 789456123
x `yokl'yokl` Prior `ha` Prior `ha` Await `ha` print ===> 987654321
This is good showcase why using compositional operators is more ergonomic than using them separately - compare this code snippet to the one above:
x `yokl` Forth `ha` Await `ha_'yokl` Forth `ha` Await `ha` print ===> 123456789
Continue: Structural wrapper subtyping