0

I'm trying to emulate a simple functional language using an actor based execution model where issues arose modelling if-expression.

Actor systems nowadays are used basically for speeding up all kind of stuff by avoiding OS locks and stalled threads or to make microservices less painful, but initially it was supposed to be an alternative model of computation in general [1][2], a contemporary take on it may be propagation networks. So this should be capable to cover any programming language construct and certainly an if, right?

While I'm aware that this is occasionally met with irritation, I saw one timid attempt to move towards recursive algorithms represented using akka actors (I've refurbished it and added further examples including the one given below). That attempt halted at function calls, but why not go further and also model operators and if conditions with actors? In fact the smalltalk language applies this model and yields a precursor of the actor concept, as has been pointed out in the accepted answers below.

Surprisingly recursive function calls aren't much of an issue, but if1 is, due to it's potentially stateful nature.

Given the clause C: if a then x else y, here's the problem:

My initial idea was, that C is an actor behaving like a function with 3 parameters (a,x,y) that returns either x or y depending on a. Being maximally parallel [2] a,x and y would be evaluated simultaneously and passed as messages to C. Now, this isn't really good, if C is the exit condition of a recursive function f, sending one branch of f in an infinite recursion. Also if x or y have side effects one can't just evaluate both of them. Let's take this recursive sum (which is not the usual factorial, stupid as such and could be made tail recursive, but that's not the point)

f(n) =  if n <= 0
      0
    else
      n + f(n -1)

Note, that I'd like to create an if-expression resembling the one of Scala, see the (spec, p. 88), or Haskell, for that matter, rather than an if-statement that relies on side-effects.

f(0) would cause 3 concurrent evaluations

  • n <= 0 (ok)

  • 0 (ok)

  • n + f(n -1) (bad, introducing the weird behavior that the call to f(n) actually will return (yielding 0) but the evaluation of its branches continues infinitely)

I can see these options from here:

  • The whole computation becomes stateful and the evaluation of either x or y only happens after a has been calculated (mandatory if x or y have side effects).

  • Some guarding mechanism gets introduced that renders x or y not applicable for arguments outside a certain range upon the call of f. They might evaluate to some "not applicable" marker instead of a value, which will not be used in C anyway, since it comes from a branch which isn't relevant.

I'm not sure at this point, If I didn't miss out on the question at a fundamental level and there are obvious other approaches that I just don't see. Input appreciated :)

Btw. see this for an exhaustive list of conditional branching in different languages, without giving their semantics, and, not exhaustive, the wiki page on conditionals, with semantics, and this for a discussion how the question at hand is an issue till down to the level of hardware.

1 I'm aware that an if could be seen as a special case of pattern matching, but then the question is, how to model the different cases of a match expression using actors. But maybe that wasn't even intended in the first place, matching is just something that every actor can do without referring to other specialized "match-actors". On the other hand it has been stated that "everything is an actor", rather confusing[2]. Btw. does anybody have a clear notion what the [#message whatever] notation is meant to be in that paper? # is irritatingly undefined. Maybe smalltak gives a hint, there it indicates a symbol.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Curiosa Globunznik
  • 3,129
  • 1
  • 16
  • 24
  • Your implementation of factorial is stateful because the recursive call is not positioned at the tail (because once `f(n-1)` returns, you still have to add something). I figure this is a fundamental problem here as well as it is in FP or imperative implementations. the technique for making accumulating recursive funcitons TCO-aware might help. (having the accumulator as an extra parameter) – Laurent LA RIZZA Oct 10 '19 at 06:28
  • I'm aware that this could be made tail recursive, it is not about the recursion or the sum, it is about the implementation of the "if". And it isn't even factorial, it's a stupid sum ;) – Curiosa Globunznik Oct 10 '19 at 06:29
  • Can you make a parallel with Prolog's `occurs_check` problem here? I think that parallel computations of if branches are a problem *per se*, especially if you want your `if` to be a guard condition in a recursive function. The aim of executing both branches concurrently and to choose the result in the end is to finish earlier by actually doing more work. But the aim of a recursion guard is to actually prevent the code from searching for things that don't exist. (which your second branch actually does) – Laurent LA RIZZA Oct 10 '19 at 06:49
  • Laurent, I'm aware that I could just NOT do the parallel computation (that's one option), and it may serve no pragmatic goal, but part of the question is, what to do in the face of it, further encouraged by the initial proposal for actor systems by Hewitt e.a. Thx for the occurs_check hint, though. And for voting for the guard approach, implicitly, or didn't you? – Curiosa Globunznik Oct 10 '19 at 06:58
  • 1
    Fair enough, I can't bring you anything further, I'm only vaguely aware of the actor model :) The crux of what I'm trying to say in my previous comments : there is a fundamental problem in the imperative `if` : it is used for two different things - actually branching, and guarding recusion. But imperative programming gets away with it by having in its semantics that it executes not both of the branches (the side-effects of the unmatched branch shall not appear in the post-state) I'd vote for the stateful solution in the case of f appearing in one of the branches, the second one in other cases. – Laurent LA RIZZA Oct 10 '19 at 07:07
  • @LaurentLARIZZA Out of curiosity: Why are you calling a non tail recursive function stateful? There are still no variables. The state is solely hold in the call stack as with any tail recursive function (even though there is only a single call stack frame in the latter case). –  Oct 10 '19 at 08:26
  • @bob: out of abuse of language probably. Every additional stack frame you keep is some state necessarily kept to pursue leftover computations. The single stack frame still necessary for TCO is the link between the recursive function and the outside world. The computation is "stateless" (maybe "functional" would be more appropriale) in the TCO version in that the state of computation is only passed as an argument to the next recursive call. – Laurent LA RIZZA Oct 10 '19 at 10:05
  • @LaurentLARIZZA Ah I see, we use the term state slightly different. As to me state is the portion of the program that changes while the program is running. In this sense the call stack isn't stateful but only holds (constant) data. Strictly speaking TCO renders the call stack stateful again, since a single frame is reused.. –  Oct 10 '19 at 10:24
  • Usually actors (in practice) are supposed to do significant operations, not emulate imperative programming constructs. But for the sake of the discussion, how would such an actor system look like? Off the top of my head I see 2 configurations: R (result) listens to 2 actors, called T and E (then and else). Then, someone sends C (the condition) to both T and E and only one responds. You basically implemented: ``R <- lambda c -> if(c) { return T(); } if(!c) { return E(); }``. Or: you have 3 actors collaborating: I, T, E. You send C to I and I triggers either T or E depending on C. – BitTickler Oct 11 '19 at 03:42
  • @curiosa Glad you mentioned haskell. Haskell is a *lazy* language. As such, the question of both ``then`` and ``else`` being executed is a non-issue. And no one in their right mind would consider a "concurrent if then else" which executes everything always as correct or useful. I know there is some difference between those computer science people who see actor model as a theory and those who actually use actors as a design element for larger systems. I am a practical guy. We once did a navigation system (automotive) which had actors like: "Router", "Positioning"; We did not have "if" actors. – BitTickler Oct 11 '19 at 03:52
  • @curiosa Exactly! And how is a workflow not imperative by its very nature? British warship in the age of sails example workflow: "If flag == WHITE_FLAG then board() else shoot_em_to_smithereens();" – BitTickler Oct 11 '19 at 04:02

2 Answers2

2

There is a little bit of a misconception in your question. In functional languages, if is not necessarily a function of three parameters. Rather, it is sometimes two functions of two parameters.

In particular, that is how the Church Encoding of Booleans works in λ-calculus: there are two functions, let's call them True and False. Both functions have two parameters. True simply returns the first argument, False simply returns the second argument.

First, let's define two functions called true and false. We could define them any way we want, they are completely arbitrary, but we will define them in a very special way which has some advantages as we will see later (I will use ECMAScript as a somewhat reasonable approximation of λ-calculus that is probably readable by a bigger portion of visitors to this site than λ-calculus itself):

const tru = (thn, _  ) => thn,
      fls = (_  , els) => els;

tru is a function with two parameters which simply ignores its second argument and returns the first. fls is also a function with two parameters which simply ignores its first argument and returns the second.

Why did we encode tru and fls this way? Well, this way, the two functions not only represent the two concepts of true and false, no, at the same time, they also represent the concept of "choice", in other words, they are also an if/then/else expression! We evaluate the if condition and pass it the then block and the else block as arguments. If the condition evaluates to tru, it will return the then block, if it evaluates to fls, it will return the else block. Here's an example:

tru(23, 42);
// => 23

This returns 23, and this:

fls(23, 42);
// => 42

returns 42, just as you would expect.

There is a wrinkle, however:

tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch

This prints both then branch and else branch! Why?

Well, it returns the return value of the first argument, but it evaluates both arguments, since ECMAScript is strict and always evaluates all arguments to a function before calling the function. IOW: it evaluates the first argument which is console.log("then branch"), which simply returns undefined and has the side-effect of printing then branch to the console, and it evaluates the second argument, which also returns undefined and prints to the console as a side-effect. Then, it returns the first undefined.

In λ-calculus, where this encoding was invented, that's not a problem: λ-calculus is pure, which means it doesn't have any side-effects; therefore you would never notice that the second argument also gets evaluated. Plus, λ-calculus is lazy (or at least, it is often evaluated under normal order), meaning, it doesn't actually evaluate arguments which aren't needed. So, IOW: in λ-calculus the second argument would never be evaluated, and if it were, we wouldn't notice.

ECMAScript, however, is strict, i.e. it always evaluates all arguments. Well, actually, not always: the if/then/else, for example, only evaluates the then branch if the condition is true and only evaluates the else branch if the condition is false. And we want to replicate this behavior with our iff. Thankfully, even though ECMAScript isn't lazy, it has a way to delay the evaluation of a piece of code, the same way almost every other language does: wrap it in a function, and if you never call that function, the code will never get executed.

So, we wrap both blocks in a function, and at the end call the function that is returned:

tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch

prints then branch and

fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch

prints else branch.

We could implement the traditional if/then/else this way:

const iff = (cnd, thn, els) => cnd(thn, els);

iff(tru, 23, 42);
// => 23

iff(fls, 23, 42);
// => 42

Again, we need some extra function wrapping when calling the iff function and the extra function call parentheses in the definition of iff, for the same reason as above:

const iff = (cnd, thn, els) => cnd(thn, els)();

iff(tru, () => console.log("then branch"), () => console.log("else branch"));
// then branch

iff(fls, () => console.log("then branch"), () => console.log("else branch"));
// else branch

Now that we have those two definitions, we can implement or. First, we look at the truth table for or: if the first operand is truthy, then the result of the expression is the same as the first operand. Otherwise, the result of the expression is the result of the second operand. In short: if the first operand is true, we return the first operand, otherwise we return the second operand:

const orr = (a, b) => iff(a, () => a, () => b);

Let's check out that it works:

orr(tru,tru);
// => tru(thn, _) {}

orr(tru,fls);
// => tru(thn, _) {}

orr(fls,tru);
// => tru(thn, _) {}

orr(fls,fls);
// => fls(_, els) {}

Great! However, that definition looks a little ugly. Remember, tru and fls already act like a conditional all by themselves, so really there is no need for iff, and thus all of that function wrapping at all:

const orr = (a, b) => a(a, b);

There you have it: or (plus other boolean operators) defined with nothing but function definitions and function calls in just a handful of lines:

const tru = (thn, _  ) => thn,
      fls = (_  , els) => els,
      orr = (a  , b  ) => a(a, b),
      nnd = (a  , b  ) => a(b, a),
      ntt = a          => a(fls, tru),
      xor = (a  , b  ) => a(ntt(b), b),
      iff = (cnd, thn, els) => cnd(thn, els)();

Unfortunately, this implementation is rather useless: there are no functions or operators in ECMAScript which return tru or fls, they all return true or false, so we can't use them with our functions. But there's still a lot we can do. For example, this is an implementation of a singly-linked list:

const cons = (hd, tl) => which => which(hd, tl),
      car  = l => l(tru),
      cdr  = l => l(fls);

You may have noticed something peculiar: tru and fls play a double role, they act both as the data values true and false, but at the same time, they also act as a conditional expression. They are data and behavior, bundled up into one … uhm … "thing" … or (dare I say) object! Does this idea of identifying data and behavior remind us of anything?

Indeed, tru and fls are objects. And, if you have ever used Smalltalk, Self, Newspeak or other pure object-oriented languages, you will have noticed that they implement booleans in exactly the same way: two objects true and false which have method named if that takes two blocks (functions, lambdas, whatever) as arguments and evaluates one of them.

Here's an example of what it might look like in Scala:

sealed abstract trait Buul {
  def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): T
  def &&&(other: ⇒ Buul): Buul
  def |||(other: ⇒ Buul): Buul
  def ntt: Buul
}

case object Tru extends Buul {
  override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): U = thn
  override def &&&(other: ⇒ Buul) = other
  override def |||(other: ⇒ Buul): this.type = this
  override def ntt = Fls
}

case object Fls extends Buul {
  override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): V = els
  override def &&&(other: ⇒ Buul): this.type = this
  override def |||(other: ⇒ Buul) = other
  override def ntt = Tru
}

object BuulExtension {
  import scala.language.implicitConversions
  implicit def boolean2Buul(b: ⇒ Boolean) = if (b) Tru else Fls
}

import BuulExtension._

(2 < 3) { println("2 is less than 3") } { println("2 is greater than 3") }
// 2 is less than 3

Given the very close relationship between OO and actors (they are pretty much the same thing, actually), which is not historically surprising (Alan Kay based Smalltalk on Carl Hewitt's PLANNER; Carl Hewitt based Actors on Alan Kay's Smalltalk), I wouldn't be surprised if this turned out to be a step in the right direction to solve your problem.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • I posted this as community wiki, since it only gives a *potential* direction for an answer, not a full answer and even not necessarily the right direction. – Jörg W Mittag Oct 10 '19 at 11:36
  • 1
    @curiosa: Smalltalk is mentioned explicitly in the paper you linked. I'm not sure I have any textual references for the other direction, but I remember Alan Kay explicitly referencing PLANNER in several interviews, in particular how the goal-directed execution model influenced the message-driven execution model of Smalltalk, which in turn was the inspiration for the message-driven execution model of Actors. – Jörg W Mittag Oct 10 '19 at 11:49
  • I considered writing the first part in Scala, but a) I already had it lying around in ECMAScript, and b) the type annotations would be distracting. – Jörg W Mittag Oct 10 '19 at 11:52
  • An objection to call an "***implementation***" ( 1st eval both, next yield one of them based on the (if-ed)-control-state ) **to reflect** the choice. This may be working in some kind of implementation, but it is not a choice as we know it. A choice to "***KILL THE BEAVER*** (to save the tree)" **OR "*KILL THE TREE***" will normally result in having either the TREE or the BEAVER kept alive. Not in the "**implementation**"-argued example above, where you will receive both a DEAD beaver AND a DEAD tree, with an ex-post advice, the code would prefer to KILL only one of 'em (a post-mortem advice) – user3666197 Oct 10 '19 at 11:54
  • Yes, but killing is a side effect, right? What about the functional interpretation, free of side effects? – Curiosa Globunznik Oct 10 '19 at 11:56
  • @JörgWMittag Isn't `tru`/`fls` just pattern matching accomplished with bare functions? –  Oct 10 '19 at 11:58
  • @user3666197: You are reading much too intricate semantics into simple pseudo-code. As I wrote in the introductory paragraphs, I am simply using ECMAScript as a somewhat more familiar notation for λ-calculus. The problem you describe does not exist in λ-calculus, since that is pure. Also, I do address that problem a couple paragraphs further down, *and fix it*. – Jörg W Mittag Oct 10 '19 at 12:00
  • @bob: Isn't `if` just crippled pattern matching? ;-) – Jörg W Mittag Oct 10 '19 at 12:01
  • **No, it is rather an evidence** of an ill-formed assumption, that an if-decision evaluation does not have to take place "before" evaluation of the choice-made. If you forbid any change of a global state-space, you may defend either a "just"-[CONCURRENT] evaluation or a true-[PARALLEL] evaluation (which, if pedantic, will then come at a cost to have to wait for the slowest evaluation-path, before leaving such true-[PARALLEL] processing-section, which is not what one would expect or call it a benefit, would it? ) – user3666197 Oct 10 '19 at 12:02
  • Note that there is another interesting connection between Actors and FP: Scheme was originally created as a vehicle for understanding Actors. The authors were already familiar with LISP, so they created a minimal LISP and then added Actors to it. Basically, in addition to `LAMBDA` for creating functions, you also had `ALPHA` for creating actors and in addition to `APPLY` for calling functions, you also had `SEND` for sending messages. The interpreter was originally written in assembly and treated actors and functions completely separately. When they realized that there was no need for `SEND` … – Jörg W Mittag Oct 10 '19 at 12:02
  • … and `APPLY` to be separate, it could just be one operation which checks the type of the argument whether it is a `LAMBDA` or an `ALPHA`. When they refactored the interpreter that way, they noticed something quite profound: both branches of the `IF` were the same! In a procedural languages with first-class procedures, lexical scoping, lexical closures, and proper tail calls, actors and lambdas are *operationally the same thing*, the difference is just whether the primitives in your library are actors or lambdas. This shows a quite deep connection between Scheme-like and actor languages. – Jörg W Mittag Oct 10 '19 at 12:05
  • @JörgWMittag But with `if` you'd need equality somewhere, whereas pattern matching is solely based on structure, right? –  Oct 10 '19 at 12:08
-1

Q : didn't ( I ) miss out on the question at a fundamental level?

Yes, you happened to have missed a cardinal point: even the functional-languages, that may otherwise enjoy the forms of AND- and/or OR-based fine-grain sorts of parallelism, do not grow as wild as not to respect the strictly [SERIAL] nature of the if ( expression1 ) expression2 [ else expression3 ]

You have spend many efforts on argumentation about recursion-case(s) whereas the principal property was left out of your view. State-fullness is the Mother Nature of the computing ( these toys are nothing but finite state automata, no matter how large the state-space might be, it is and always will principally remain finite ).

Even the cited Scala p.88 confirms this: "The conditional expression is evaluated by evaluating first e1. If this evaluates to true, the result of evaluating e2 is returned, otherwise the result of evaluating e3 is returned." - which is a pure-[SERIAL] process-recipe ( one step after another ).

One may remember, that even the evaluation of expression1 may have (and does have ) state-change effects ( not only "side-effects" ), but indeed state-change effects ( PRNG-steps into a new state whenever a random-number was asked to get generated and many similar situations )

Thus the if e1 then e2 else e3 has to obey a pure-[SERIAL] implementation, no matter what benefits may be brought into action from fine-grain {AND|OR}-based-parallelism ( may see many working examples thereof in languages that can use 'em right since late 70-ies early 80-ies )

user3666197
  • 1
  • 6
  • 50
  • 92
  • Thx for the effort, but dunno. You see, e.g. a parallel evaluation of if `a > b then a - b else b - a` isn't a problem at all IMO. I'd also appreciate an answer bringing light to it from the actor perspective. To clarify: I didn't want to mimmick the execution model of Scala, it's about having an expression instead of a statement. "many working examples thereof ...": of serial implementations? Of course, but If you could name a parallel one I'd be glad. Could you also corroborate your claim of "has to obey a pure-serial implementation..." with further work or references? Doesn't seem obvious. – Curiosa Globunznik Oct 10 '19 at 11:15