Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Effect Managers #146

Open
wende opened this issue Jul 9, 2017 · 13 comments
Open

Implement Effect Managers #146

wende opened this issue Jul 9, 2017 · 13 comments

Comments

@wende
Copy link
Owner

wende commented Jul 9, 2017

Undeniably the EM is the toughest task to grasp in the entirety of Elchemy project. One of the reason being it's the most complex element of Elm, and the other that back-end isn't as domain specific and the need for different EMs might potentially be much bigger.

As the Elm-core team states: 10 EMs should be enough to cover everything that might happen from the outer world. My assumption is on backend the number might be a little higher (if finite at all)

This topic is mostly for discussion regarding what EM's might we need and whether - and if, then how - to deviate from original Elm implementation.

There might be a finite count of EM's which would be (as far from what I can think of):

  • File system
  • Environmental Variables
  • Outgoing sockets
    • HTTP
    • TCP
    • UDP
    • Websockets
  • Incoming sockets
    • HTTP
    • TCP
    • UDP
    • Websockets

With the biggest caveat being databases. Although they all operate on either HTTP, TCP or UDP it would be madness to require all of the client libraries to be rewritten in EM idiomatic style - ergo each database would probably require its own EM.
OR
We could try to write EM for Ecto. Which would be an amazing experiment

Same goes for Phoenix - or at least Plug. They probably should be wrapped too

My status right now on Effect Managers implementation - I'm afraid it has to be the same what Elm does - but we can try the hardest to provide documentation (which Elm misses for reasons) and nice templates on how to wrap existing Elixir/Erlang goodies

So far the best sources I could find on how effects work are these:
https://medium.com/@alex.lew/8e87fd14332c

And these repositories:
https://github.com/fredcy/localstorage
https://github.com/eeue56/elm-http-server/blob/master/src/HttpServer.elm

@OvermindDL1
Copy link

I can easily see an Effect Manager being a Process on the BEAM since they communicate to the main elm 'application' via messages back and forth and all.

@wende wende modified the milestone: Elchemy 0.5 Jul 16, 2017
@wende
Copy link
Owner Author

wende commented Jul 17, 2017

@OvermindDL1
The Effect Managers aren't the problem.

Probably the biggest one is a difference in front-end's and back-end's architecture.
Elm on Front-end uses single "master-app" with one global state and multiple EMs.
For obvious reasons Erlang has much different approach, with the state being as split as possible.
That generates a little bit different environment.
Where there are two "levels" of sife-effects. Inner side-effects (Message passing between processes) which is of smaller impurity (Time of execution is the only variable that could affect the results), and classic side-effects which are generated from outside (described above)

And although I've had some discussion with Elm's community and some of them stand for superiority of single global state, I believe it's not a way to go and Elchemy shouldn't compromise the biggest advantage of using Erlang as a platform.

Obviously we could just treat internal message passing with the same restrictions as the external side effects, but I believe that could put some future great ideas to danger (namely f.i Time Travel debugging), since it's a crucial part of every Erlang system and forbidding message passing would render entire system unusable.

This topic without a doubt has to be treated with extraordinary diligence and before any changes are made permanent it's worth to do as much testing as possible.

Also I believe I changed my mind about following Elm's approach in here, but I still have to make my mind for sure.
The main reason being: Elm's EMs are hard to implement for a reason, which is a limited need for them.
Backend doesn't necessarily qualifies as truthful to this statement. Even though all of the effects are indeed limited to these sources, expecting each of the services like different DataBases or HTTP APIs to be reimplemented from scratch would be very unhealthy for the project and also just purely impractical.

I believe this is a place where our roadmap should slightly skew from Elm's road and although we'll still keep consistency with all of the syntax, the implementation of Effect Managers should be handled in different manner.

I'll get back to you when I discover more in terms of functional solutions to the problem.
You're welcome to take sides in this discussion, I'd appreciate to hear all the concerns and ideas

@OvermindDL1
Copy link

I entirely agree that global state is bad, for certain.

As for the effect, let me give some reference.

Elm's Effects are not named that because they are 'effectful', but rather it is a style of (incomplete) Algebraic Effects, and I'll demonstrate AE's here shortly, but I really think they should be kept and brought to the fore-front.

First, what is an Algebraic Effect, let me start with an example.
Let's say that we want to implement a, say, mutable reference cell, think the IO monad in Haskell or ref in OCaml (or, say, the pdict in Elixir), let's do this in a more traditional Effects syntax (unlike Elm's oddity):

module State (S : sig type t end) : STATE with type t = S.t = struct
  type t = S.t

  effect Put : t -> unit
  let put v = perform (Put v)

  effect Get : t
  let get () = perform Get

  let run f ~init =
    let comp =
      match f () with
      | () -> (fun s -> ())
      | effect (Put s') k -> (fun s -> continue k () s')
      | effect Get k -> (fun s -> continue k s s)
    in comp init
end

I first define 2 'effect's, Put, which takes a type t and returns nothing, and a Get, that returns a type t, I define a couple helper functions to perform the effects, and I make the effect 'processing loop'. You can see the parallels with Elm's effect module here, as there you define a type for inputs and outputs (traditionally an ADT/SumType for Elm, as it is for the above example), and you have a central 'loop' that receives the effect messages. In this example there is much less implicit (it is all explicit here unlike Elm, which hides it behind a lot of magic, the above is actual valid MC-OCaml Code).

Traditionally, you'd use the above code by defining the type you want (yay first-class modules, Elm entirely lacks those) by doing, for integers and strings:

module IS = State (struct type t = int end)
module SS = State (struct type t = string end)

So I've 'finalized' the State module into two named modules named IS and SS for int and string respectively. To use them I just call them before I call the main function that can call the effects (of get and put):

let foo () : unit =
  printf "%d\n" (IS.get ());
  IS.put 42;
  printf "%d\n" (IS.get ());
  IS.put 21;
  printf "%d\n" (IS.get ());
  SS.put "hello";
  printf "%s\n" (SS.get ());
  SS.put "world";
  printf "%s\n" (SS.get ())

let _ = IS.run (fun () -> SS.run foo "") 0

So just define a function foo to run, and you run it in SS.run, which is run within IS.run. In traditional Algebraic Effects you call the 'body' function 'inside' of the effect runs. When an effect is performed, like via the first IS.put 42;, what is done is think of it like an exception, the 'state' of the execution is bundled up at this point as a normal continuation, which is passed to the effect loop above, and for the put case it calls the | effect (Put s') k -> (fun s -> continue k () s'), so the Put with the argument to it of 42 is passed in, as is the continuation passed in to k, and here we just return a function that takes the current state value (at this point it is 0, then 'continues' the continuation of k, with arguments of nothing (), with the updated state of s (this looks a lot like an Elm effect module without the magic eh, you see the same message handlers and all), and at this continue call the function picks back up where it left off.

The handling is very low level above, traditionally an effect module is in another thread/process and messages are passed between.

In fact, if you look at the generated Elm javascript, it indeed starts up the effect managers as a kind of secondary 'process' in javascript and passes messages to/from it via the internal Elm Router.

But this precisely models an Actor/Process in Elixir and I think are a great representation of it. An effect module is just a 'named process' (which on the BEAM can be just the module name of the effect module). And thanks to how the BEAM works you don't need that central 'main' call that Elm uses, that can just be it's own use.

Here's another example, here is a cooporative greenthreading of a round-robin scheduler entirely within Algebraic Effects, first let's define the interface it should be (so we can change the internals, like to a priority scheduler for example:

(* Control operations on threads *)
val fork  : (unit -> unit) -> unit
val yield : unit -> unit
(* Runs the scheduler. *)
val run   : (unit -> unit) -> unit

And define the effect types:

effect Fork  : (unit -> unit) -> unit
effect Yield : unit

And the helpers:

let fork f = perform (Fork f)
let yield () = perform Yield

And finally the effect 'message handler' (in elm parlance):

let run main =
  let run_q = Queue.create () in
  let enqueue k = Queue.push k run_q in
  let rec dequeue () =
    if Queue.is_empty run_q then ()
    else continue (Queue.pop run_q) ()
  in
  let rec spawn f =
    match f () with
    | () -> dequeue ()
    | exception e ->
        print_string (to_string e);
        dequeue ()
    | effect Yield k ->
        enqueue k; dequeue ()
    | effect (Fork f) k ->
        enqueue k; spawn f
  in
  spawn main

And it can be used like (this generates a binary tree and prints out it's number, with each node being it's own greenthread:

let log = Printf.printf

let rec f id depth =
  log "Starting number %i\n%!" id;
  if depth > 0 then begin
    log "Forking number %i\n%!" (id * 2 + 1);
    Sched.fork (fun () -> f (id * 2 + 1) (depth - 1));
    log "Forking number %i\n%!" (id * 2 + 2);
    Sched.fork (fun () -> f (id * 2 + 2) (depth - 1))
  end else begin
    log "Yielding in number %i\n%!" id;
    Sched.yield ();
    log "Resumed number %i\n%!" id;
  end;
  log "Finishing number %i\n%!" id

let () = Sched.run (fun () -> f 0 2)

So it is this:
image
And as such, it prints out:

Starting number 0
Forking number 1
Starting number 1
Forking number 3
Starting number 3
Yielding in number 3
Forking number 2
Starting number 2
Forking number 5
Starting number 5
Yielding in number 5
Forking number 4
Starting number 4
Yielding in number 4
Resumed number 3
Finishing number 3
Finishing number 0
Forking number 6
Starting number 6
Yielding in number 6
Resumed number 5
Finishing number 5
Finishing number 1
Resumed number 4
Finishing number 4
Finishing number 2
Resumed number 6
Finishing number 6

AE's are a very powerful concept, can represent anything from mutation (like subscriptions in Elm) to threads to all sorts of other things.

I honestly believe they should be brought more to the fore-front on the BEAM, since they fit the actor model so very well. :-)

@wende
Copy link
Owner Author

wende commented Jul 18, 2017

@OvermindDL1 WOW!
This is some incredibly valuable input. Hands down!
Although OCamls syntax is quite foreign to me I was able to understand the concept.

Of course I agree that we still should use Algebraic Effects.
I'll probably take some more time to read and thoroughly understand the concept you just shared with us.

I'd really love to see your suggestions on how we could achieve an explicit implementation of Algebraic Effects usage that would still conform to Elm's syntax (because elm-make)
Would you like to come up with a suggestion of an exemplar implementation which would show how a module using a supposedly implemented Effects System might look in Elchemy?

That would give us the best context to talk about explicitness, readability and easiness of entry of different solutions

@OvermindDL1
Copy link

I honestly think that Elm's normal Effect Modules make Algebraic Effects quite naturally. It is a bit more verbose than the lower-level MC-OCaml (oddly enough) but it is clearer, I'd say keep them. :-)

They could even transparently be turned into GenServers, or perhaps via special GenServer declarations inside the effect module. Could even do other things like supervisors or so as well, or just plain processes. Since effect modules already pass messages in and out, and the messages are well typed, then it becomes entirely safe and typed and verifiable by the Elm compiler. :-)

@wende wende removed this from the Elchemy 0.5 milestone Aug 18, 2017
@wende
Copy link
Owner Author

wende commented Aug 29, 2017

I've just implemented something that might work as a type-safe GenServer.
https://gist.github.com/wende/2a515b81ea49f7d33e0a5a2e7e4667c6
I'd gladly hear some suggestions on how to make it better

@OvermindDL1
Copy link

I'll look at it more tomorrow, but why not just use the normal effect system of Elm but map it to a GenServer? It would allow a more concise syntax, still have the bouncers, although everything would become 'casts's then unless decorated some other way, hmm...

@wende
Copy link
Owner Author

wende commented Aug 30, 2017

@OvermindDL1 Exactly. The last sentence is what made me think for last couple of weeks whether all-casts is a good way to go and the longer I thought about it the less it seemed so.
The example above addresses that, and allows for running both atomic calls and regular casts.
It's definitely different approach to what Elm does. But honestly I'm leaning towards it more and more with each example I can think off.
Elm doesn't have to struggle with parallelism, but we definitely do. And OTP constructs already solve them so ideally I'd want to have their equivalents in Elchemy

@OvermindDL1
Copy link

Actually an idea. You know that Elm Effects take a message as a subscription or direct call, perhaps if the first argument back is a special PID type as a return message, then it would be a call, else it would be a cast. Hmm...

The current way you show is generally more powerful overall if a little verbose, can always build helpers around it in the future to fix that. :-)

@wende
Copy link
Owner Author

wende commented Aug 30, 2017

@OvermindDL1
The problem with sending a message back as a result of the call is that if it just sends a message back it's no longer atomic.
A call blocks until it replies. If it returned a message then not only it wouldn't block but also it would put the message on the top of the message queue.

So imagine a code like this

server
>>= setToTimeNow
>>= set 0

Where setToTimeNow executes a task of getting the Time.now and then sending a message back to our server of set now
If all of them were indeed the casts and Time.now would be a time-taking computation then depending on the timing the state of our server could be either current time or 0, because if Time.now took more time then executing of the next line of code then the message could arrive after next line's set 0

This couldn't happen in a single-threaded environment. But it definitely can on Erlang VM.

Now assuming a setToTimeNow is a call and blocks for as long as the Time.now runs there is no possibility for a race condition

Not to mention how easy it is to inline multiple tasks in one process without the entire sending to updateLoop
For instance given an implementation of a sum of two random integers. In this implementation it'd be:

get2RandomsSum : Process number
get2RandomsSum =
    Task.map2 (+) RandomGenerator.randomInt RandomGenerator.randomInt

I'd like to see a similar implementation in Elm to convince me.
Because this is the discussion on Tasks vs Cmds from Google groups
screen shot 2017-08-30 at 17 53 18
And the latter post is much more convincing to me
https://groups.google.com/forum/#!topic/elm-dev/MEBzD3f7Bq8

@OvermindDL1
Copy link

A call blocks until it replies. If it returned a message then not only it wouldn't block but also it would put the message on the top of the message queue.

Except this is not how GenServer's work. ^.^

A GenServer call is just a cast where it passes in self() too, then the GenServer calls handle_call instead of handle_cast, then just takes the return value from that and passes it back to the caller. The GenServer call itself just receive waits for the return message or a timeout (5 seconds by default). Everything is message on the BEAM, even GenServer call's. :-)

Remember, you can pass a pid in casts too, it is just a different internal message, hence why I say copy that style via some special types. :-)

Cmd's, Tasks, and Subscriptions are emulateable in a fully concurrent manner on the BEAM. :-)

@wende
Copy link
Owner Author

wende commented Aug 30, 2017

@OvermindDL1 I might have been not clear enough about my point.
I'm not talking about the implementation of the call and cast.
I'm fully aware of how GenServers work, but I'm talking about the practical usage of their casts and calls in different scenarios. And please mind that we don't allow for receive blocks in Elchemy so we have to be careful about emulating them well enough.

What I was trying to represent is if we imagined we've got 2 GenServers talking to each other. Where one holds a state and the other one only returns Time.now. Let's call them A (state) and B (time).
And a GenServer has a cast message to set its state to time.now.

1. A ---- give me current time ----> B
2. A <----- returns current time  --- B
3. A ------- set time to var(now) --> A

If A wasn't waiting for B to return between 1. and 2. then something could send it different messages during that wait and break the atomicity of the call.
But if the Task is executed in the process of A and also blocks it for the time of execution then we no longer have that problem and properly type-safely emulated a call from GenServer

//Edit
Ok. Now when I read my previous post I can see where the confusion comes from. It's terribly worded and can suggest a completely different meaning to what I intended

@wende wende added this to the Elchemy 0.5 milestone Oct 15, 2017
@wende wende self-assigned this Nov 24, 2017
@wende wende changed the title Discuss Effect Managers Implement Effect Managers Dec 3, 2017
@wende
Copy link
Owner Author

wende commented Jul 18, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants