addamsson
11/28/2020, 10:03 PMJannis
11/29/2020, 1:01 AMoperator 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!)Jannis
11/29/2020, 1:01 AM<>
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^^):
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.Jannis
11/29/2020, 1:02 AMraulraja
11/29/2020, 10:04 AMaddamsson
11/29/2020, 10:16 AMaddamsson
11/29/2020, 10:16 AMaddamsson
11/29/2020, 10:20 AMLast
construct looks like a variable to me, no?addamsson
11/29/2020, 10:20 AMraulraja
11/29/2020, 10:30 AMraulraja
11/29/2020, 10:38 AMraulraja
11/29/2020, 10:38 AMaddamsson
11/29/2020, 10:41 AMaddamsson
11/29/2020, 10:41 AMraulraja
11/29/2020, 10:42 AMraulraja
11/29/2020, 10:45 AMaddamsson
11/29/2020, 10:45 AMfun interface
is not possible right nowraulraja
11/29/2020, 10:46 AMaddamsson
11/29/2020, 10:46 AMraulraja
11/29/2020, 10:46 AMaddamsson
11/29/2020, 10:46 AMaddamsson
11/29/2020, 10:46 AMraulraja
11/29/2020, 10:46 AMaddamsson
11/29/2020, 10:47 AMraulraja
11/29/2020, 10:48 AMraulraja
11/29/2020, 10:49 AMraulraja
11/29/2020, 10:49 AMraulraja
11/29/2020, 10:50 AMraulraja
11/29/2020, 10:50 AMraulraja
11/29/2020, 10:51 AMraulraja
11/29/2020, 10:51 AMraulraja
11/29/2020, 10:52 AMraulraja
11/29/2020, 10:52 AMaddamsson
11/29/2020, 11:01 AMaddamsson
11/29/2020, 11:01 AMsuspend
?addamsson
11/29/2020, 11:02 AMbecause kinds are only useful to deal with methods like map or flatMap polymorphicallydo you mean that some laws (like having an
empty
function) can't be represented with an interface
?addamsson
11/29/2020, 11:02 AMIf 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 instancei bumped into this even without Arrow...is there a solution for this problem?
Jannis
11/29/2020, 11:41 AMhow would you write the above Haskell code in Kotlin? I can't really make sense of itSome of the things:
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:
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)addamsson
11/29/2020, 4:46 PMLast
is like a variable, right? It only "remembers" the last value you setJannis
11/29/2020, 5:00 PMJannis
11/29/2020, 5:02 PMaddamsson
11/29/2020, 9:12 PMaddamsson
11/29/2020, 9:13 PM