defmodule Coro do @doc "Create a new coroutine" @spec create((pid -> any())) :: pid def create(f) do IO.puts "create" coro_driver = fn -> # Block until we are resumed receive do {:resume, resumer} -> IO.puts("Resumed for the first time") f.(resumer) other -> raise "Invalid initial resume" end end spawn(coro_driver) end @doc "Yield to the thing that resumed this coro" @spec yield(pid, any) :: nil def yield(resumer, value) do # Having to pass the resumer explicitly is wonkity but it's a start IO.puts "yield" send(resumer, {:yield, self(), value}) # Block waiting for resumption receive do {:resume, resumer} -> IO.puts("wossname") other -> raise "Invalid resume" end end @doc "Resume the given coro" @spec resume(pid) :: any def resume(coro) do IO.puts "resume" # We need to monitor a process to detect if it quits without yielding to us, # see https://www.erlang.org/doc/reference_manual/processes#monitors :erlang.monitor(:process, coro) send(coro, {:resume, self()}) receive do # The coro yielded to us {:yield, pid, value} -> IO.puts("Coro #{inspect pid} yielded to us with value #{inspect value}") # The coro exited without yielding {:DOWN, ref, _process, _pid2, reason} -> IO.puts "Coro exited normally #{inspect reason}" :erlang.demonitor(ref) other -> raise "aw heck: #{inspect other}" end end end x = Coro.create( fn resumer -> IO.puts "Hello world from inside a coro" Coro.yield(resumer, :hi) 3 end) Coro.resume(x) Coro.resume(x)