Witchcraft to get something similar to the IO Monad (in Haskell, Scala, etc) but in Elixir?

Background

I have recently been delving into more functional code. My objective right now is to get something similar to the IO Monad (in Haskell, Scala, etc) but in Elixir.

To this extent, I understand Witchcraft should be in theory capable of doing it.

Doubts

However, after reading through their documentation I have some questions:

  • I was not able to find a clear-cut example of the IO Monad. I have the idea what I have to create Monads myself using one of the sub-libraries, but I am not 100% sure of this.
  • I am not sure if Dialzyer plays nice with Witchcraft and if it can detect issues if my code is incorrect (like Scala compiler does).

Could someone help me answer these questions?

3 Likes

From skimming the docs, it looks like you only have the abstract Monad typeclass available and you would have to define specific monads, as you noted.

Personally, I would build up strength in two languages in parallel, if I had your goals. Use Elixir ecosystem as is for some projects and use Purescript or Haskell for projects when you want full purity.

1 Like

I’ve always loved Elixir’s mission statement of “practicality”.

My first university level programming course was Haskell… and I hated it; I couldn’t wait to get to the C++ courses, which at the time, were much more practical.

It doesn’t bother me that you can’t do the y-combinator in Elixir, or any of the stuff that requires an anonymous function to refer to itself.

So it would seem my initial observation was somewhat correct. Thanks for the input!

I will politely disagree :smiley:

2 Likes

IO monad do not make sense in Elixir. In Haskell it is used to mark a function that will have side-effects that handle, well, IO. In Elixir you have no way to mark function as doing IO, as it is not really possible to detect whether such function will or will not do IO at all.

1 Like

What would you want it to do? In Haskell the IO monad largely has two uses:

  • To express dependencies between different side effecting function calls so that the correct ordering of side effects is maintained in a lazy environment.
  • To enable static tracking of side effects.

Elixir isn’t lazy and doesn’t have Haskell’s static analysis, so what would you want to do with an IO monad?

Unfortunately it does not detect issues statically as Haskell does.

2 Likes

I am looking for a concept that proves my code does what I expected it to do without surprises in a mathematical way, thus allowing compile time checks to be effective.

To my knowledge, the IO monad is the most widely known concept to achieve both things:

  • Allows my code to be proven correct via category theory
  • It makes compile-time checks possible

It also has the nice side effect of making it very visible that “This function will do something funky, like talking to the outside world, and you should be aware of it”.

Elixir does not need to have Haskell’s static analysis. With tools like dialyzer (and Gradient) these days, compile time checks are very much possible and depending on the tool you choose, can have a great level of accuracy when detecting errors.

This brings the natural question:

  • Are there any other concepts in the programming world, that can give me (the flawed programmer) the same guarantees as IO Monads do? (To mathematically prove at compile time that my code is not going to wake me up at 4AM because a call to the outside horrible world had an unforeseen effect?)

If the answer is “yes, concept X does that as well”, then I am all ears. Thus far I have checked “Functional Core, Imperative Shell” architecture as a different solution to this problem, but my explorations found some issues that let me to continue the search.

Mainly, I am a terrible, fleshy human being that will make mistakes, and I want a fire-way proof to let me know that ahead of time.

Is this because of the usage of Macros?
I was able to create simple data-structures that replicate the Either and Option monads in Elixir and work well with Dialyzer. However I also understand that IO Monads are a different beast.

1 Like

It’s more the other way around. The static type system makes the IO monad and those checks possible.

Elixir doesn’t yet have a robust static type system so you cannot statically prove properties of your program. There’s work on adding types and other forms of static analysis to Elixir but today you’ll need to look at other BEAM languages such as Gleam or Purerl.

It looks like Witchcraft focuses on the polymorphism aspect of type classes rather than the static verification.

The IO monad only tracks side effects and ordering, so it’s not enough to get that property. You need the more general tool of a robust static type system.

For specifically side effects I think algebraic effects would fit well with Elixir as they are easier to use, have less runtime cost, and map well only existing Elixir code. They don’t do ordering but in a strictly evaluated language that is unimportant.

Macros make implementing a type system more challenging but it’s not a show-stopper, it is still possible.

If you are happy with the level of analysis that Dialyzer offers you then you can implement an IO monad in Elixir without too much pain!

defmodule IOMonad do
  @opaque io(a) :: %__MODULE__{__effect__: (-> a)}
  defstruct :__effect__

  @spec pure((-> a)) :: io(a)
  def pure(effect) do
    %__MODULE__{__effect__: effect}
  end

  @spec map(io(a), (a -> b)) :: io(b)
  def map(io, transform) do
    pure(fn -> transform.(io.__effect__.()))
  end

  @spec bind(io(a), (a -> io(b))) :: io(b)
  def bind(io, transform) do
    pure(fn ->
      transform.(io.__effect__.()).__effect__.()
    end)
  end
end

The same rules apply as in Haskell:

  1. You must wrap all side effecting code in the IO monad rather than performing it immediately.
  2. You must never access __effect__ anywhere in your code, you must always use map and bind.

This unfortunately will be very challenging in Elixir. Unlike in Haskell there is little to verify you are using it correctly, and you’ll have to fork or wrap any libraries you use that have side effects, including the standard library.

Algebraic effects could be used with existing Elixir code without modification but require a more powerful static analysis tool than exists for Elixir today.

edit

As a bonus here’s the same thing in Gleam. The nice thing about this is that the compiler will check that you use it correctly!

pub opaque type Io(a) {
  Io(effect: fn() -> a)
}

pub fn pure(effect) {
  Io(effect)
}

pub fn map(io: Io(a), transform: fn(a) -> b) -> Io(b) {
  Io(fn() { transform(io.effect()) })
}

pub fn bind(io: Io(a), transform: fn(a) -> Io(b)) -> Io(b) {
  Io(fn() { transform(io.effect()).effect() })
}
5 Likes

What about Gradient? For a gradual typing system, it can be pretty accurate.

Do you know of any libraries or resources that implement/explain this concept?

What is the main difference between Gleam’s static type system, and the usage of code 100% annotated with Gradualizer (for the exliir equivalent, we can use Gradient). They would achieve basically the same thing iirc, and currently Elixir has a huge active community, with events conferences and give ways, which Gleam does not have yet (not at the level of Elixir).


On a side note, I find the concept of both Gleam and Alpaca (both have the creators very active in this forum) incredible attractive but the the thing that keeps me from doing a more in depth exploration is mostly the fact I would lose almost everything Elixir offers:

  • the community
  • a place to ask for help where people actually are active and post
  • the contests and book give aways
  • and most importantly: job offers

This does not mean both languages are inferior. They may very well be superior, but if I try them, like them, and start doing projects in them, then soon my Github page and Linkedin profiles will lose its appeal to recruiters :S

1 Like

:eyes: but I thought…

5 Likes

Bad choice of words from my part, you got me there !
What I mean is, the code using IO monads in Scala/haskell also gives me extra information with no additional cost.

No real side effects happen! :smiley:

Gradient/Gradualizer would do much better! I’m not sure how complete it is but I’m quite excited for it. Looks great.

It’s more of a type checker or compiler feature than a library. The nice thing about it is that for just inferring and restricting effects you don’t need to change the code at all.

Here’s some normal Elixir code

defmodule Main do
  def main do
    text = IO.gets("What's your name?")
    IO.puts("Hello, " <> name)
  end
end

Here it is with an IO monad

defmodule Main do
  def main do
    IOMonad.pure(fn -> IO.gets("What's your name?") end)
    |> IOMonad.map(fn name -> IO.puts("Hello " <> name) end)
    |> IOMonad.unsafe_perform_io()
  end
end

And here it is with algebraic effects.

defmodule Main do
  def main do
    text = IO.gets("What's your name?")
    IO.puts("Hello, " <> name)
  end
end

There’s no changes from the normal code to use IO with this system! All you need to do is annotate functions with any effects they emit.

defmodule MyApp do
  @effects [:console]
  def print_string(string) do
    IO.puts(string)
  end

  @effects [:send_message, :receive_message]
  def run do
    send(self(), "Hello")
    receive do
      x -> x
    end
  end
end

Any functions that call these functions will automatically be detected as also having these effects.

They will be approximately the same. A gradual type system with full annotations should be as sound as Gleam’s type system (with or without annotations) though it comes down to the details of Gradualizer specifically.

Thank you for your support :purple_heart:

The Gleam discord server is often a good place to chat about Gleam or to ask questions. It is quite active these days

5 Likes

At this point, I am interested in algebraic effects in Elixir, but I need to read more literature on the topic to make a decision.

For the time being however, I will leave this topic here, and when the time comes, I will create a new thread just focused on algebraic effects.

For now, I will conclude my study on Monads, mostly on the basis of the Zeigarnik effect.

Thank you for this information though!

1 Like

Is Gradient/Gradualizer still being worked on? I am super interested in that, but last I checked, it seemed to have stalled out? Or rather, it’s still considered experimental and not much change has been made? I would love to have gradual typing!

Seems to have some activity on GitHub

Gradient is still being worked on. I try to contribute with bug reports and documentation, if you wanna join I ma sure help would be welcome !

1 Like