Hacker News new | past | comments | ask | show | jobs | submit login

I find it amusing that the system described in this article is simply what Haskell calls the ‘IO monad’. That is to say, it describes an IO system with the following ingredients:

1. Each IO operation returns a special value which denotes an IO action

2. These IO actions can be sequenced using a function which feeds an IO action to a continuation

3. Syntax sugar to remove the annoyance of explicitly writing the sequencing function

Together, (1) and (2) make this a monad; (3) in this article is just ‘do’-notation. The only major difference from Haskell is that Roc doesn’t generalise the idea to other monads, which makes it far less powerful than Haskell’s Monad typeclass.




This gets discussed in the Roc community. They are exploring designing a language without higher kinded polymorphism.

Here's a snippet from the Roc FAQ.

> It's impossible for a programming language to be neutral on this. If the language doesn't support HKP, nobody can implement a Monad typeclass (or equivalent) in any way that can be expected to catch on. Advocacy to add HKP to the language will inevitably follow. If the language does support HKP, one or more alternate standard libraries built around monads will inevitably follow, along with corresponding cultural changes. (See Scala for example.) Culturally, to support HKP is to take a side, and to decline to support it is also to take a side.

https://www.roc-lang.org/faq.html#higher-kinded-polymorphism


I feel like adding HKP isn't "taking a side" nearly as much as not adding HKP to turn people away because you don't like their cultural contribution.

By adding the feature you aren't actually excluding the people who don't want to use it. They are free to avoid it and maintain parallel ecosystems where HKP is avoided.

You are only excluding them if the standard library forces them to interact with HKP code, which arguably Haskell does, and it makes a PITA for teaching it. Haskell however isn't doing it with the intention of exclusion.

On the other hand, not adding the feature because you don't like the culture it brings is intentionally exclusionary. You're intentionally taking a side against certain kinds of people and the certain kinds of code they want to be able to express without repetition... and you're hamstringing the expressibility of your language to do it.

I completely understand wanting to avoid HKP for teaching purposes, but maybe that is a problem that could better be resolved by always providing monomorphic options for standard library functions....

But avoiding HKP to avoid a certain kind of culture? Well now it's personal!


As the FAQ entry notes, one of the "sides" in the design space here is "a fragmented ecosystem is best because there's no other way to accommodate the variety of different styles people want to use" - this is a totally reasonable preference to have!

Still, hopefully it's clear why a language designer might want to go down a different path. After all, it's also totally reasonable to prefer an ecosystem which isn't fragmented along these lines (the way it is in, say, Scala) even though it means fewer different programming styles are supported.


Roc has algebraic effects instead of Monads for effect tracking. The two systems are exactly as expressive as each other [1]. Algebraic effects compose better than Monads, and are probably more beginner friendly. Imo Monadic code can be a little easier to follow sometimes since there’s less non-local control flow. They are both great things to have in a language and I’m super excited to see how much progress Roc is making in both approachability and performance when it comes to pure functional programming.

[1]: https://homepages.inf.ed.ac.uk/slindley/papers/effmondel-jfp...


Roc actually doesn't use algebraic effects under the hood, although errors can naturally accumulate in a way that makes the code look more like error handling in algebraic effects systems:

https://www.roc-lang.org/tutorial#task-failure

The implementation detail that makes this error handling possible is that Roc uses anonymous sum types called "tags" (OCaml calls them polymorphic variants) that can accumulate on the fly:

https://www.roc-lang.org/tutorial#tags


Very interesting, thanks for the clarification. What is the difference between algebraic effects and rocs approach?


In Haskell terms, Roc's `Task ok err` is essentially an `IO (Either err ok)` - the only difference between how Roc does it and how Haskell does it is that `Task` bakes in error handling whereas `IO` doesn't. (Plus the syntax sugar being different, of course.)

So the biggest difference is that algebraic effects are a separate language feature, whereas Task (like IO in Haskell) is a plain old type in the standard library that happens to represent effeects.

Incidentally, the Task.await function in Roc is based on Elm's Task.andThen - we just tried out a different name and argument order for it:

https://package.elm-lang.org/packages/elm/core/latest/Task#a...


can someone explain in plain English and with some easy to understand code examples?


But do algebraic effects preserve referential transparency? It’s hard for me to see that they do.


IIUIR, Roc's syntax is more ergonomic than do-notation.

In Roc, you can put a "!" (bind) pretty much anywhere, whereas in do-notation it must be part of a binding.

    current_user <- fetch_user
    manager <- fetch_manager current_user
Compared to

    manager <- fetch_manager (!fetch_user)
Please correct me if I'm wrong!


You don't need to bind everything. The corresponding Haskell code is

    manager <- fetch_manager =<< fetch_user


Why have do-notation if we can use fancy operators for everything?

I think this version would is less approachable for most developers.


one advantage of this over do-notation is brought up in the article (though not explicitly): you can bind multiple things in a single expression. In Haskell, you'd have to do

    do x <- getx; y <- gety; x+y
In roc you can do

    getx! + gety!
which looks a bit cleaner.


This is really just a matter of how much syntax sugar you want to implement. Idris already has precisely this syntax, and there’s a proposal to add it to Haskell too [0]. But none of this changes the core properties of the system which make it monadic.

[0] https://github.com/ghc-proposals/ghc-proposals/issues/527


TIL that Idris also has a very similar `!` operator (in the prefix position instead of suffix) - thank you for sharing!


Even Javascript has a similar syntax, though `yield` / `await` only works for certain "monads" (promises seem close enough)


  (+) <$> getx <*> gety


Definitely doesn't look as clean as the example above, or as clean as the Swift equivalent of

    await x + y


The same syntax works across a range of constructs in Haskell, Scala and Idris.

  (+) <$> getx <*> gety
This could be:

* summing two reads in a Transaction,

* summing two parsed numbers in a Parser,

* summing two Nullable values into one Nullable value,

* producing a Validated<T> by validating two of its parts which require validation,

* and on and on.

It's even the cartesian product when run on lists:

  let suits = ["♠", "♥", "♦", "♣"]
  let nums  = concat [["A"], map show [2..10], ["J","Q","K"]]
  let cards = (++) <$> nums <*> suits

  putStrLn (unwords (take 15 cards))
  A♠ A♥ A♦ A♣ 2♠ 2♥ 2♦ 2♣ 3♠ 3♥ 3♦ 3♣ 4♠ 4♥ 4♦
Swift, Roc, Rust and JS have specialised their syntax for just one of these concerns.

Also, it looks like you can just do it with ! in Haskell: https://hackage.haskell.org/package/monadic-bang


> it looks like you can just do it with ! in Haskell

Yes, although it's slightly clumsy.

    example = do
      let suits = ["♠", "♥", "♦", "♣"]
      let nums  = concat [["A"], map show [2..10], ["J","Q","K"]]
      let cards = do pure (!nums ++ !suits)
      putStrLn (unwords (take 15 cards))

    ghci> example 
    A♠ A♥ A♦ A♣ 2♠ 2♥ 2♦ 2♣ 3♠ 3♥ 3♦ 3♣ 4♠ 4♥ 4♦


The Roc version is still cleaner.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: