i'd love to hear it!
# arrow
a
i'd love to hear it!
j
In the haskell world this is the most convenient way of creating a builder. The boilerplate was mostly to help understand whats going on, afaik haskell can easily derive most of that. In kotlin this specific pattern isn't quite as useful because a) its more expensive (immutability has its cost) and b) its not always as easy to use although with an infix combine function or simply using
operator plus
it may not be so bad. Also kotlin lacks newtypes entirely so defining types like
Last/First, Min/Max, Sum/Product
isn't as nice. I would still try to identify monoids wherever possible and try to check them against the 3 laws. Finding a datatype is a lawful monoid usually is a sign of a well defined datatype! For me the biggest wins from spotting and using a monoid are: • Being able to use law 3 to distribute and thus partition how I evaluate the type I use.
IO<A>
is a monoid in which
IO a <> IO b = IO (a <> b) in an oversimplified world
. If I have
a + b + c + d
I can freely partition this into
(a + b) + (c + d)
which I can distribute among threads. This insight my look trivial on
+
but the second you spot a more complex monoid on a type you previously accumulated manually in a fold, you can easily parallelise it without thinking about correctness as monoid laws already provide that. • Composing monoids to create complex behavior just from mixing types: The builder example used
Last (Option Name)
which combines two monoids
Last
and
Option
and thus creates a new monoid with the notion of a missing name and a combinator that always keeps the most recent added name. This style is recommended mostly on intermediate values such as a builder that will then be converted into a final product. In the haskell world, the above also leads to zero boilerplate as all the typeclass instances get derived by the compiler 😅 Sooo, this probably became quite unstructured as its mostly a braindump on what I currently think about monoids, sorry if it got somewhat incoherent in places^^ Monoids are easily the most useful typeclass for me, monads etc don't even come close (especially since monads are monoids!)
Sooo, now on a laptop! Monoids: You are right that it defines both an empty value/operation and a way to combine values (
<>
in haskell land,
plus
in arrow). Some easy to grasp examples are: • Integer addition (empty = 0, combine = +) • Integer multiplication (empty = 1, combine = *) • Maximum (empty = -Infinity, combine = max) But these two operations are not enough to define a Monoid instance, it also has to follow 3 laws: •
empty + x = x
x + empty = x
(these are not equivalent because Monoids don't have to be commutative) •
a + (b + c) = (a + b) + c
This is what makes monoids incredibly useful, especially the third law allows us to, with a guarantee for correctness, split any chain of monoidal operations and distribute the work, perform optimizations and in general allow us a lot of freedom on what assumptions we can make when using the monoid instance. (More restrictions (laws) => more freedom when using them, as contradicting as that sounds) These laws also allow us to simplify such expressions and due to being able to execute them in any order (given that they are then combined in the same order again) we can also optimize for that! So how are monoids used: • Every fold is a monoidal operation. You pass an empty/initial value and a way to combine. Following monoid laws alone we can combine Folds and/or distribute them however we want. There is a haskell package that offers combinators which can be used to build streaming folds that compose and work in one pass. Accumulating and counting in one go? No problem just compose them:
runFold (sumFold <> countFold) values
. Thanks to monoid laws (and some others regarding foldables) this can be done generically and still be single pass. This usually requires manual combination in a lot of langs. • Monoids make for a great api. Thanks to their laws, the combination of elements is straightforward and easy to reason about, which makes a clear and well defined way of combining elements. This is regardless of what this combination does, so long as it follows monoid laws. • Monads are monoids! The sentence
Monads are just monoids in the category of endofunctors
isn't just to mock haskellers^^ Functions are also monoids and so are functions wrapped in context (which is basically what this tells us in overly simplified words). Also, even if you code may not need it, if you can spot a monoid instance, define it: A type that has a well defined monoid instance is usually a sign of a good implementation for that type. Suppose we have a builder for something (sorry for the haskell, its just shorter^^):
Copy code
data Builder = Builder (Last (Maybe Name)) (Last (Maybe Password)) (Max (Maybe Level)) (Min (Maybe Timeout))

instance Monoid Builder where
  mempty = Builder mempty mempty mempty mempty
  Builder name1 pw1 lvl1 tim1 <> Builder name2 pw2 lvl2 tim2 = Builder (name1 <> name2) (pw1 <> pw2) (lvl1 <> lvl2) (tim1 <> tim2)

defaultBuilder :: Builder
defaultBuilder = mempty

name :: Name -> Builder
name n = mempty { name = (Last (Just n)) }

-- ... same for all others

-- Usage:
build :: Builder -> ActualConfig
build :: ...

build $ defaultConfig
  <> name "MyName"
  <> password "HelloWorld"
  <> level Master
  <> level Noob
  <> time (seconds 10)
  <> time (seconds 2)
The final result due to the instance will be
Builder "MyName" "HelloWorld" Master (2 seconds)
This contains a few new things: •
Last
is a monoid if its content is also a monoid:
empty = Last empty
(just wrap),
Last _ <> Last b = Last b
Simply choose the "last" element •
Option
is a monoid if its contents are a monoid as well:
empty = None
Some a <> Some b = Some (a <> b); None <> _ = None; _ <> None = None
Password
and all the other types include some monoid instances as well, but are hardly relevant.
I did not know slack had a max message length and automatically splits long messages...
😂 3
r
Thanks so much Jannis, this is great
a
Thanks @Jannis for the writeup this was very insightful!
I can't read Haskell code though 😞
this
Last
construct looks like a variable to me, no?
if it retains the last value it is like a variable I overwrite
r
@Jannis newtyping monoid just got easier in 1.4
It’s up to the callers to choose which function to use to satisfy it
a
that's because of the SAM upgrade, right?
@Jannis how would you write the above Haskell code in Kotlin? I can't really make sense of it
r
@addamsson yes, also in the same spirit this is what a data type will look like when we remove kinds (not the complete api) but in terms of encoding shows how we are adapting the relevant pieces of the functor hierarchy to fun interface as well https://gist.github.com/raulraja/e732bdaace04426d3be4380b7760a8c0
Since data types do not need to implement the sam interface, they just do so by member presence or structure of extension functions. With this we can offer an efficient inline API that works with suspension and has no dispatch to type-classes since it propagates inlining all the way up to the fun interface extension methods
a
wait,
fun interface
is not possible right now
r
it is since 1.4
a
oh
r
but it’s not encoded in Arrow yet
a
how did i miss that
😄
r
this is the proposed encoding to remove kinds
a
why did we have kinds in the first place?
r
because all of our encoding for monad bind and others depended on the style of polymorphisms where type classes have a similar shape as in Scala or Haskell
Both of those have kinds and ad hoc polymorphism but only the second is possible in Kotlin with fun interface
Furthermore now we have continuations we don’t need kinds because kinds are only useful to deal with methods like map or flatMap polymorphically
with continuations we have monad invoke like in the either blocks
and that since it gives you A in F<A> eliminates the need to map or flatMap
If Kotlin had kinds natively methods like Iterable.flatMap would not need to return a List<A>, they could have returned a F<A> where F has a valid functor instance
Since kotlin does not include kinds we added them emulated but that imposes the users having to deal with .fix and also requires two dynamic dispatches to the typeclasses
we are attempting to remove all that in favor of this simpler approach that is now possible since 1.4
And since we have arrow-continuations
a
can you show me an example of this continuation?
is this because of
suspend
?
because kinds are only useful to deal with methods like map or flatMap polymorphically
do you mean that some laws (like having an
empty
function) can't be represented with an
interface
?
If Kotlin had kinds natively methods like Iterable.flatMap would not need to return a List<A>, they could have returned a F<A> where F has a valid functor instance
i bumped into this even without Arrow...is there a solution for this problem?
j
how would you write the above Haskell code in Kotlin? I can't really make sense of it
Some of the things:
Copy code
inline class Last<A>(val a: A) {
  companion object {
    fun <A>monoid(am: Monoid<A>): Monoid<Last<A>> = object : Monoid<Last<A>> {
      fun empty(): Last<A> = Last(am.empty())
      operator fun Last<A>.plus(other: Last<A>): Last<A> = other
    }
  }
}
Last
only encodes how
plus
behaves and nothing else.
inline
isn't great in kotlin but its closest to what haskell `newtype`does, its a wrapper only for behavior (and ideally that wrapper is inline and removed by the compiler)
Max/Min/Sum/Product
are all similar just with different implementations for
empty
and
+
. Again tho: This style is super useful in haskell, but not too nice in kotlin. Arrow Meta will probably make it usable in kotlin as well, but for now I would not recommend it. The builder example in kotlin is probably close to this:
Copy code
data class Builder(val name: Last<Name?>, val password: Last<Password?>, ...) {
  companion object {
    fun monoid() = object : Monoid<Builder> {
      fun empty = Builder(Last(null), Last(null), ...)
      operator fun Builder.plus(other: Builder) = Builder(name + other.name, password + other.password /* This uses the monoid instances of Last, which I did not want to write out fully */, ...)
    }

    fun name(name: Name) = empty().copy(name = Last(name))
    // other similar smart constructors...
  }
}

// use
fun Builder.build(): SomeActualObject = ...
val result = Builder.monoid().run {
  empty()
    + name("Hello")
    + password("World")
    + ...
}
This won't compile, but thats only to keep it short, kotlin will have more boilerplate than haskell. Also as raul noted we are exploring an encoding that may still look typeclass structures but works/looks very different to make full use of kotlin rather than trying to emulate haskell (which isn't bad in itself, haskell is amazing, but hard to do in certain cases and typeclasses is one of them)
a
so
Last
is like a variable, right? It only "remembers" the last value you set
j
That's one way to view it, yes There is also a newtype for First somewhere in haskell libs that has the opposite behavior.
Only thing I dislike about this analogy is that a newtype is an actual immutable value, but other than that it's fine 👍
👍 2
a
i'm gonna read my book...
i understand like half of these things