Hi all, I'm a total beginner to Roc, but I know some other functional programming languages. I was reading the FAQ and I got really curious about the cultural implications of monads.
From https://www.roc-lang.org/faq.html:
Considering that these are the only three options, an early decision in Roc's design—not only on a technical level, but on a cultural level as well—was to make it clear that the plan is for Roc never to support HKP. The hope is that this clarity can save a lot of community members' time that would otherwise be spent on advocacy or arguing between the two sides of the divide. Again, it's completely reasonable for anyone to have a different preference, but given that languages can only choose one of these options, it seems clear that the right choice for Roc is for it to never have higher-kinded polymorphism.
Obviously I am on the HKP of the divide that allows defining Monads inside the language. I am so deep in my bubble that I cannot understand why it makes a difference. I thought the divide was between functional and imperative paradigms, not between monads and antimonads. Could someone explain where this dismay for a language feature comes from, so I could better understand both sides of the coin? Thanks and best wishes.
I think there are several points, but for me it is keeping the language small. Roc is a descendant of Elm, which also does not have HKP. What I love about both Roc and Elm is that it is so small that I can basically keep the whole language in my head. Also it's easier for beginners. I tried to learn Haskell, but that monad stuff was too strange to me. I just did not understand the purpose of it. After learning Elm, it was quite easy to see the common pattern, that is called a monad. Not a big difference in practice, if I use a special bind operator or a specific function for the type, that can also have a nice name.
Also there was something about the type system not being always decidable with HKP? But I'm not sure about that.
For me it means a more accessible language. I got into Roc because it is a simple language and I think that is a big benefit. I had no functional background but was able to learn it easily. Basically if you have seen recursion and pattern matching, there isn't much extra learning to do with Roc. I spent like a whole day trying to understand Monads (because I got the impression that I need to do that before I can learn Haskell, or any functional programming). Didn't succeed, so didn't learn Haskell.
So I see the divide as "Mondas scare off non functional programmers, but make hardcore (to me that's the right word) functional programmers happy". Since the goal of Roc is to be widely adopted, it needs to appeal to non functional programmers.
I'm a bit confused. I would consider Task a monad. So I would say that roc has monads, just not higher kinded types.
Higher kinded types are required for generic interfaces that work with all monads, but they aren't required for monads. It just means that the bind-like methods are specific to each monad.
For example Task.await
is a specific implementation of bind.
Brendan Hansknecht said:
Higher kinded types are required for generic interfaces that work with all monads, but they aren't required for monads. It just means that the bind-like methods are specific to each monad.
Yeah, by "monads" I meant being able to define the whole Monad
typeclass/trait/interface inside the language, so that Task
and Result
and IO
can use the same interface. I would explain it as the interface of doing stuff in a context, be it async context, context where an error can be thrown or the context of the standard input/output. Each statement is not only doing what it says it does, it also has an additional effect in the background context.
Noew I'm not sure if the message was for me Brendan, but I'll send it anyways. My point is that I don't need to know that it is a monad. I admit, it may not be the best mentality, but at this stage of my fp journey, I don't want to learn them. It is hard enaugh to shift my programming thinking to expressions with immutability where there were statements before. I'm sure it would be awesome to create my own functions that would work withe the proposed !
operator (if I understand correctly that would require HKP) instead of only Task.await (or was it attempt?) and Result.try having access to it, but I am just not there yet.
I think one of the main reasons is that with a Monad type class, there would likely be multiple versions of libraries that either use arbitrary monads or do not. These would not work well together and would fracture the eco system. Roc prefers a more direct style, and by default everyone will use that style if the other one is not an option.
Norbert Hajagos said:
Yeah, the !
operator and the await operator are doing a similar thing and I don't like code repetition. But thh practicality appears when you have to deal with two contexts at once, e.g. you have an async function that can throw an error. When you would return a Task in a Result it is always possible to turn it into a Result in a Task. The same with Result and IO actions that can fail and write it in the console. These examples could be implemented for arbitrary Monads, so you would get all such converters for free.
Isaac Van Doren said:
Roc prefers a more direct style, and by default everyone will use that style if the other one is not an option.
Thanks for the answer, it makes sense that not everyone would implement a Monad typeclass if they wouldn't want to use it as a monad.
Just to clarify, !
and Task.await are not doing a similar things. They are the exact same thing.
!
desugars to |> Task.await
.
An extra note. We are consider making !
a generic desugaring. As such, it really is equivalent to a higher kinded bind operator.
But thh practicality appears when you have to deal with two contexts at once, e.g. you have an async function that can throw an error. When you would return a Task in a Result it is always possible to turn it into a Result in a Task. The same with Result and IO actions that can fail and write it in the console. These examples could be implemented for arbitrary Monads, so you would get all such converters for free.
100%. We require more explicit support and lose some forms of composability by not support higher kinded types. We also avoid a lot of compiler complexity and specialization costs. It is definitely a trade off.
Looking at your specific examples:
Task
in roc.|> Task.onErr \_ -> Stderr.line "Something went wrong!"
Last updated: Jul 06 2025 at 12:14 UTC