I think the key insight of Smalltalk can be grasped just by thinking about how Smalltalk does branching. A line from the GNU Smalltalk tutorial:
mybool ifTrue: [ ^ 2 / 4 roundup ]
The outer part of this expression reads “send a closure expression, as the message ifTrue, to the receiver mybool.”
Keep in mind, this `mybool` could be anything! It just so happens that there are two runtime objects, `true` and `false`, that satisfy the “Boolean interface” (consisting of messages ifTrue and ifFalse) the way you’d expect—with `true` running any closure passed in an ifTrue message, and `false` running any closure passed in an ifFalse message. (For those of you who know combinatory logic, these objects are essentially the K and SK combinators.)
But consider that you can create your own object that implements this interface—with whatever arbitrary logic you like—and substitute it for a Boolean as the value of `mybool`, and no code will be able to tell the difference.
This is what is meant by “computation as messaging.” Branching is a request to an opaque “branch-evaluating object.” At each step of computation, you’re not getting the runtime to compute—you’re getting your objects to compute. The objects are the computers, and the messages are the instructions to those computers!
That sounds very interesting. How are return values treated under this model, though? Is the object you send the message to obligated to send something back, synchronously?
But the object responding to a message can also "reply later" if one or more of the arguments of the message are "blocks" which are function-like objects created by the sender, having access to variables in the context of the sender. The recipient of a block can call it any time in the future, including multiple times.
So to be clear, it's not like a closure where the local value is "copied" into the closure, but instead it continues to reference the original variable, including any updates to it?
Smalltalk "blocks" (meaning instances of the class BlockCloures) are much like inner functions in JavaScript, they can read and write variables in the lexical scope where the block(-closure) was defined/created.
Are the variables "copied"? I would say so because if you call the containing method multiple times the same code will create new block-closures and they will refer to the variable environment that existed when the block was created, so they will each have their own separate set of outer scope variables.
Unlike JavaScript inner functions, Smalltalk closures can cause a return from the containing method (but need not).
Thinking in terms of C-style variables may be unhelpful. Instead, consider Smalltalk “variables” as scoped names bound to heap-allocated objects. You can bind as many names as you like to a given object at any time, and each name is usable within the bounds and duration of its scope.
What the VM does under the hood is more complex than that, natch, but as long as the user-level abstractions don’t leak (which they shouldn’t) the mental model works.
No, I don't think that's right. Grandparent just said the closure is returned, and you can call it later to make it block and get a result. Sounds like a future.
I'm actually not sure whether Smalltalk passes arguments by value or reference. But they're definitely not constantly updating like spreadsheets or FRP behaviors.
Something I have gotten more attuned to over time is the importance of the message format - i.e. the medium - in defining the message.
That is, the way in which the message is specified changes the classes of errors and development challenges one might encounter. If function arguments are positional you get positional errors; if they are named you enter the business of defining many more types. Messages may return a different version of themselves(e.g. arithmetic) or a different category of message. The later in time the message is delivered, the more it has to carry around its own context. And when a message has unbounded scope it has unbounded complexity.
And like with a lot of things, there aren't silver bullets. Many things can be defined as a single enumerated
value. Sometimes tuples are nice, sometimes records are nice. Sometimes the problem expands to the point where you have to have a message with a little computer inside it - but when that happens it's usually not for the purpose of any specific task, it's to add a layer of programmability into the system.
I've toss in cap'n'proto & surely others who let you write messages that talk about still outstanding messages. Still short of referring to full on streams in your messaging.
Thanks for pointing that out! I'd heard of Cap'n'Proto but didn't realize how cool it was. Pipelining promises is incredibly useful! I'll check it out more deeply now.
When I first heard of it, maybe I was initially skeptical because of the name, having been traumatized years ago by Cap'n Software Forth on my Apple ][, which John Draper developed to implement SleazyWriter while incarcerated in the Alameda County Jail.
>EasyWriter was a word processor first written for the Apple II series computer in 1979, the first word processor for that platform. It was written by John Draper's Cap'n Software, which also produced a version of Forth, which EasyWriter was developed in.
>Draper developed EasyWriter while serving nights in the Alameda County Jail under a work furlough program.
Interesting. Hadn’t heard of Cap’n’Proto. A clever idea, although I do wonder if it’s being too clever for its own good, being unnecessarily restrictive.
Dr William Cook (who designed much of Mac OS’s “RPC-plus-queries” IPC model) explored a more generalized approach where whole self-contained chunks of behavior can be sent across the wire for remote evaluation. As I recall, his Batches model uses a “safe” subset of familiar JavaScript operations, but any behaviors could be supported as long as they are guaranteed to terminate.
There’s nothing inherently magical about this: it’s just a carefully-restricted form of remote execution where the client-supplied “code” to be executed is incapable of doing nasty things like access restricted APIs or lock /blow up the server with infinite loops or stack overflows. Beyond that, it’s just a question of whose CPU you want to burn more, and how much time you want to spend bouncing between them.
Thinking about it from a JavaScript or C mindset will make your head ’splode, but for Lispers (of which Smalltalkers are a spinoff) it’ll be a big fat “how obvious”. Be nice to see it further pursued.
He explicitly credited the actor model as one of the major influences on Smalltalk and OOP. The distinction as far as I have been able to tease it out is that in the actor model there is a distinction in kind between actors and values passed among actors. In OOP, there is no distinction in kind, that is, imagine you have an actor-like thing that sends you a reference to something, but that something is going to be of the same category of actor-like thing.
Hewitt created actors after hearing an early Smalltalk talk by Kay at MIT, as I recall it. That wasn't the only influence (capabilities were a big one, for instance) and I'd be surprised if there wasn't reciprocal influence, though.
In Hewitt's actor model, messages are also (immutable) actors. At least in the early version I read about.
The programming language Scheme was originally named Schemer against Actors ;-)
The monad (https://wiki.haskell.org/Monad) is another mostly unsuccessful lambda calculus way to try to address some of the issues resolved in the Actor Model.
My english is failing me, what's the meaning behind 'schemers against actors' ?
I only knew about actors for its scheme influence and the fact that it was highly concurrent (even this I'm not so sure). I got into college way too late for it to be studied sadly.
The original name of the programming language Scheme was "Schemer", which was shortened by removing the last character because of file system limitations. The motivation for the name Schemer was that the authors were scheming against Actors analogous to the programming Conniver, where the authors were conniving against Planner.
That had been my conclusions over the years. Erlang (or Elixir) are ironically one of the nicest environments for OO code these days (given they are usually categorized as "functional")
The YC ideation talk with Alan Kay has a small rant from Alan about what he meant with Objects, and how that isn't what objects are used for now. What he described sounds exactly like the actor model.
I can't find the quote online so this could easily be apocrypha or something I made up in my head, but I could have sworn Alan Kay has said as much along the way.
There's a quote from Joe Armstrong saying something like that
> As Erlang became popular we were often asked "Is Erlang OO" - well, of course the true answer was "No of course not" - but we didn't to say this out loud - so we invented a serious of ingenious ways of answering the question that were designed to give the impression that Erlang was (sort of) OO (If you waved your hands a lot) but not really (If you listened to what we actually said, and read the small print carefully).
>If a language technology is so bad that it creates a new industry to solve problems of its own making then it must be a good idea for the guys who want to make money.
Erlang made important contributions. However, the Erlang model needs the following important extensions:
•Automatically reclamation of storage of an unreachable future [Baker and Hewitt 1977] (e.g. process in Erlang). For example, an Erlang process can be orphaned if its Process Identifier (PID) becomes inaccessible. Also, since Erlang is not strongly typed, cyberattacks can be launched against dangling references to processes that no longer exist.
•Language support for holes in the region of mutual exclusion of an Actor implementation. For example, in Erlang it is necessary for application programmers to explicitly code a message handler for each re-entry into the region of mutual exclusion potentially enabling cyberattacks from outside the process.
Given that a PID can be sent to another node and even serialized and deserialized, it doesn't seem possible to know if it's still referenced in a meaningful way.
For example, if process X on node A sends a message to process Y on node B and the network goes down, that doesn't mean process Y won't send a message to process X when the network resume.
This can be mitigated by processes not being "objects".
Especially objects that need to be pinned by ref counting.
My personal interpretation of the Actor model is a message passing system to model the real world.
In the real world I can shout out "make me a sandwich" and even if I perceive that there is a process that could potentially satisfy that request, I also work on the assumption that for any number of reasons... the channel of communication going away, misenterpretation, the agent or myself dying, the resources not being available or delays due to manufacturing may impede progress.
That is just the way the real world works.
Although Future/Promise can be built easily on top of Actor, they are optimistic at best - especially in diatributed cases; which, if my interpretation is correct is your reason for Actor in the first place.
Actor is powerful so in my opinion, future/promise and building ref-counts on top undermines the core of it.
Pat Helland, after numerous years of distributed transaction work completely did a 180 in the face of reality.
There is nothing wrong with Actor.
As far as strong typing, I personally believe that is a completely orthogonal conversation. I just cannot see the conflation.
Strongly typed to me means I send an argument to a function and it happens to match the signature.
Well, if I have a strongly typed function that takes two integers and somehow is supposed to return the sum. I guess it is helpful to know that as the user-agent (caller) of the function, but that knowledge only helps programmers, not computers.
Strong typing is definitely not a model of reality when dealing with humans as functions, and often humans are end-point "processes" (heh) of function calls.
Not that I am against strong typing in any way. I just believe the Actor model is cleaner without mixing things up.
Certainly agree Erlang is great but not the ultimate.
Well... my 2c and thanks to you, Alan, Joe, Hoare, Sussman, Gelernter et al. et al. for the inspirations over the years, and appreciate you plugging away at the models to this day.
Yes and no. Carl hewit will tell you that erlang's actor model is not pure (and to be sure a process doesn't have to be much of an actor - like Elixir's Task module). But I think that impurity is one of the strengths of the BEAM.
So I would qualify 'best' to meant "easiest system that 95% looks like actors that you can quickly get up and running and doing useful things and or play around with"
In erlang based systems stateful network protocols and concurrent user stories are all modeled with actors and one million connections per node for websockets is a thing.
I deployed an actor-based system model to completely rearchitect and replace an inefficient stateless python system. There have been zero runtime or concurrency errors since I deployed (unfortunately sometimes the hardware the actors are modeling fail us) but patches to work around the hardware failures are simple.
The Actor Model has been used in many important large applications [Bernstein, Bykov, Geller, Kliot, and Thelin 2014; Bonér and Reisz 2017, Cesarini 2019] and has become the foundation for current computing.
Satisfactory implementation for Reusable Scalable Intelligent Systems with millions of Actor cores for quadrillions of Actor are under development, but not yet publicly available.
I understand the accidental aspect. I've accidentally now written a spreadsheet while replacing a set of Makefiles with a more readable dependency graph of files. Now I'm adding a graphing component...
how do you handle persistence ? i’m trying to design a system at the moment and i feel like actors are great until you have to deal with persisting state to a database ( at which point the idea of having millions of independant process each hitting your database to save and load their state look like a disaster)
edit : i misread your post, thought you were designing a system, not a PL... maybe you can still answer my question ?
What i mean by persistent is : what is the best practice to save an actor’s state so that it can be properly restored in case of a crash without loosing information.
I think i’m still not clear on how you would design a transactional system ( such as order & paiement processing ) with actors in a way that won’t make it look like a microservice based system ( aka : one per subtask, fetching info from a db for each incoming request, and storing the result in a db in the end)
It seemed to me actors had to have a more fine grained context ( such as one per order), but in that case i’m wondering how it’s supposed to handle saving its state regularely so that no information on the order processing state is ever lost
Excellent question! But your question seems to make the unfounded assumption that application programmers should be in charge of restoring crashed Actors. Instead, an Actor System should restore Actors as best it can based on recordings that it has. Applications must be made resilient against any and all kinds of inconsistencies that will pervasively arise.
Do you know any resource i could read that would explain how all that is supposed to work out in practice ?
I've never heard of an actor system able to automatically respawn actors with their previous state (the whole system would look like a tree of cached data, each layer responsible for saving the leafs under it, with a huge "persist to DB" on top , wouldn't it ?). Does erlang OTP do those kind of things ?
Edit: also, are you Prof "Carl" Hewitt ? The one that invented actors ? I'd be honored you found a question of mine about actors excellent...
- not all states transitions need to be durable. If performance matters you’ll have to get your actors to call something like a « saveState » from time to time, otherwise every single property change is going to have an overhead in terms of performance.
- what does « durable » mean ? Surely, just having the state of an actor saved in the memory of the supervisor isn’t enough. If the two are on the same server and the server shuts down, it’s game over. You need a « persistence » service / actor, but that thing is going to have to persist the state of all the actors in your system. I don’t see how that can work if you’ve got millions of actors running in parallel.
That's not how you would persist to the database. Processes (Erlang VM threads) have mailboxes that process messages (like records to be saved to db) in a queue. Everything is built around message passing, each process just handles its own narrow job.
You should check how Erlang / Elixir database drivers handle that.
In my experience with Elixir there are never many processes that have a state to save in a database. Eventually it's the same load no matter if the implementation is actor based or object oriented.
The general idea is to build the transaction in a pipeline of function calls. Each function gets the old definition of the transaction as an argument and returns a new one. Eventually the call at the end of the pipeline executes the statements in the definition, wrapping them in a transaction.
If the question is how to deal with 100 or 100 k concurrent transactions, the answer again is in the same way a cluster of servers running programs written in any other language do. Eventually SQL is SQL.
Every time this comes up it makes me want to learn Smalltalk, but the current tooling/IDEs don’t seem to have been kept up to date. Is it possible to learn Smalltalk without a Smalltalk-specific IDE? Or, is there a relatively small current language that is similar to Smalltalk?
Smalltalk was always a system first, language second. That said, if you want to ignore half (not all!) of what makes Smalltalk interesting, there's always GNU Smalltalk ("Smalltalk for people who can type"):
To add to this comment; if you use Pharo, the impressive part is right in the first-run tutorial. It gives you the basics but you have to write the code to update the environment to give you the next/previous buttons for each step.
You can run whatever code you want in those windows, but just by finishing the tutorial you'll have an idea of what you're getting into.
It's on my backlog of things to toy with, along with Prolog. And after playing with Objective-C long enough I quite enjoy the syntax.
I can definitely highly recommend Prolog for playing. I found it to be a mind-opener in the same way learning Scheme for the first time was a mind-opener. Programming with predicates/relations is extremely powerful.
As a teaser, consider a predicate Concat(X, Y, Z). In Prolog, capitalized identifiers are considered as unbound variables. There aren't return values; instead, you compute via unification.
So we can approximate the idea of a list concat funtion by entering something like `Concat([1, 2], [3, 4], X).` The Prolog evaluator will answer that X must be [1, 2, 3, 4] in order for the predicate to hold true. Looks like a standard function with a quirky way to provide an out param.
But these are predicates, not functions. Concat's internal logic defines a logical relation among the various values. This logic holds no matter which values you leave unbound. So here are some other things you get for free:
`Concat(X, Y, [1, 2, 3, 4]).` -- this will, in turn, iterate through all possible values of X and Y that make this predicate true. Thus Prolog will give us X: [], Y: [1,2,3,4]; but also X: [1], Y: [2,3,4]; and so on.
`Concat(X, [3, 4], [1, 2, 3, 4]).` -- this will give us X: [1, 2]; alternatively, it will give us the answer 'no' (sort of equivalent to false) if [3, 4] is not some tail of the provided list.
`Concat([1, 2], [3, 4], [1, 2, 3, 4]).` -- will give us "yes", for true.
This is a simple example, but now perhaps the potential power of Prolog is more visible. Imagine the possibilities -- for instance, a constructed grammar can tell you not only if a given candidate sentence is valid, but it can also (in theory) generate every single valid sentence in turn.
I've been feeling this out through the Adventures in Prolog book. It's also a fascinating insight into how SQL engines work and what it means to write declarative code.
I've had an idea for a prolog side-project for a while, but just haven't learned enough yet to know how I could implement it. In my mind, reserving a slot in a calendar or otherwise finding the next best opportunity is a valid use-case.
"Cuis is a free Smalltalk-80 environment with a specific set of goals: being simple and powerful. It is also portable to any platform, fast and efficient."
“Operating System: An operating system is a collection of things that don't fit into a language. There shouldn't be one.” — Daniel Ingalls “Design Principals Behind Smalltalk” [0]
Learning the Smalltalk “world”/vm is an essential part of learning Smalltalk the language. Many people are initially put off by this but if you try to find a “Smalltalk without a Smalltalk-specific IDE” you are missing out on an enormous part of what Smalltalk is.
It's kind of amazing to me how few how little our work has been in developing "extras", general services, between language & user.
The os has become a passive tool to run stuff on, but once it was an involved middle layer that the user might be tapping throughout their use, with the os & user capable of understanding exposing & controlling some of the items in the program space. Now the program is more of a blind space to the os, doing it's own thing. Adjusting a program's volume & resizing it's window is all the interaction most users project from os space into program space.
The frustrating thing is - the more you learn about what the OS can do, the more you realize that many programming languages and the applications written in them quite often reimplement large parts of the OS for an astonishingly large portion of their codebase. That isolates developers from learning about the OS in the first place. (They always end up needing to learn about it, anyhow, so it's a bit futile. Now, however, they feel smug about it and pretend like they shouldn't need to learn all this OS stuff.)
I guess developers always implement their own ignorance to a certain degree (and how could they not!? except, you know, spending more time learning their craft). What they don't know the OS can do, they will recreate - often poorly - in their own software. It does get rather comical and the various frameworks that you can subscribe to in modern programming languages repeat the concept, just that now, you're reimplementing things that the language can already do, but in the framework.
My favorite example is the various kinds of "service" patterns, like dependency injection - in many instances that I've seen, you could boil most of its use down to simple function calls. But since global functions are ew, you instead create a service that you inject and then call a method in. Boom, the gods of OOP are happy and nobody is allowed to say: "Wait a minute, if I take a few steps back and squint a little, what you're doing looks surprisingly similar to a global function call."
(And yes, I know, DI is supposed to be about overriding functions, yada yada very few people actually do that and it's often such an organizational hassle with downstream problems that projects eventually resort to put up conventions from doing that most of the time. Anyways - tell that to the people that I've seen implement, I kid you not, a CamelCaseService.)
DI services are usually abstracted behind interfaces - and most systems allow you to replace the service interface implementation at runtime.
So if you have a class that uploads files to a cloud storage provider - it would make use of IStorageProvider, and you might have AmazonS3StorageProvider, AzureBlobStorageProvider, LocalDiskStorageProvider, and for testing you would have DevNullStorageProvider.
A program using DI without interfaces with different implementations is missing the other advantages of DI.
Ingalls was completely correct: It should be Actors all the way down (and Actor Systems all the way up).
Soon there will be packages with millions of Actor cores for quadrillions of Actors. Operating systems, e.g. Linux, will be inadequate for Reusable Sacalable Intelligent Systems.
I disagree, a system implemented at the language level this way can in principle provide far stronger security guarantees than a typical operating system. Security issues can still come up obviously but they could essentially be isolated to the language runtime.
Bear in mind, it’s Ben about a decade since I last used Smalltalk, but at the time there was a web framework called Seaside based completely on using stored closures instead of normal Ajax.
Digging into the code, I found that the VM just gives a code block access to all top level objects by default, but that isn’t a requirement. You can instantiate your own “global” and run the block within that. Back then I believe it was being used because old closures needed to see old top level class objects that might have been changed in the meanwhile. So you can use this to sandbox functions.
I would imagine it isn’t perfect, but it could be made so.
Instantly was enticed by this, but quickly ran into the depths. I'm on NixOS unstable and I realize this is not ever going to be a well-supported place to be in terms of running random software, but thankfully there are in fact Pharo attributes in nixpkgs. Sadly though, I can't figure out how to use it.
It looks like I want to start with Pharo Launcher, so I've installed the Pharo VM and Pharo Launcher, but when I try to start Pharo Launcher a window pops up and it just exists. All I can see in the terminal is a message about pthread_setschedparam failing, which does not seem fatal.
I wonder whether my problem is with not understanding what I'm doing, or with NixOS, or something in between...
I have the VM seemingly working through the NixOS package, and if I run the Pharo image downloaded from there, it seems to show a window with nothing on it but a menu item labelled "Pharo" that does nothing.
Perhaps I'm missing something but I doubt the Linux VM download on that page will help me. Ordinary binaries don't tend to run on NixOS without at least some ELF patching due to the Nix store architecture.
I can’t help you. I sometimes have NixOS running in a VirtualBox container, but don’t have one available right now. Do you have a plain Linux running anywhere, or as a container inside NixOS? Except for occasional font problems, Pharo works fine on Linux.
The IDE is where the super powers are. Like any IDE it just takes a little time. https://pharo.org/ has an online course to get into it.
Being in a live system is unlike anything else - the first time a debugger appears and you fix the bug in the debugger and just hit the “continue running” button is a rush. TDD inside the debugger is amazing. And do much more.
You really need to experience it, else it just sounds like hot reloading features of current tech.
Being that other languages are Turing-complete ...
Which doesn't really mean much when it comes to the standard developer experience and how that differs between programming a live image and the tooling built around that versus manipulating text and the tooling built around that. One lives at the abstraction level of running code and the other at the level of syntax.
In C/C++, I can do live debugging, reverse-time debugging, edit-and-continue debugging, multi-device debugging, multi-threading debugging... We also got language servers, on the fly retesting and recompiling, all kinds of profiling... All that with free or open tools readily available, not to mention paid, proprietary ones.
In higher-level languages, fancy features are even more prevalent.
Smalltalk is an IDE + runtime together on a single VM. This brings many advantages (very fast software development; almost no compile time; you can change the running system, ...), but also disadvantages (harder to integrate external libraries, hard to separate final runtime/binary from development)...
Wren is a small scripting language with a bit of Smalltalk flavor, though syntax is more like Ruby. It's written by the author of Crafting Interpreters so if you've read that (particularly the C chapters) you'll understand quite a bit about the implementation.
Something like Pharo is more like learning a whole new OS in addition to a language. Unless you have the energy to do that, it's probably best left on the bucketlist. I bailed.
Along with the edifying stuff there is a lot of incidental difference in the OS that is not edifying. That's why I mentioned energy. You need a lot of energy to be able to follow through and make a proper go of things on a different planet.
The way messages are constructed in Smalltalk is one of its delightful features. It seems so obvious to 'template' a message with the replaceable parts being the parameters. Typical methods a la C++ or Java look so clumsy in comparison.
It would be nice if there was a way to construct something similar around actor-based systems on the JVM.
I’m very disappointed when I see new languages (cough Rust) not even consider named arguments. The added readability at call sites just seems like a no-brainer to me.
The only counter-argument I've heard was that it has the consequence of making the argument name part of the API (i.e. changing the name would be a breaking change, so all parameter names of exported functions "leak" into the public interface). That always seemed like a very minor drawback to me, I too would appreciate to have named arguments in Rust.
It seems more like a feature to me. Sometimes parameters change slightly and you will never notice it until you re-read the docs. So if the change is explicit I can go look at it during compile time and not be surprised by it during runtime.
Of course there are occasions where the name is unfortunate (typo, ambiguous name or incorrect term) but the docs could mention it and it would not be the end of the world.
I agree, although I haven't done enough work with languages using named parameters to really judge how inconvenient they can become in practice.
It seems like most naming issues could be alleviated by allowing the developer to provide aliases, this way you can change the preferred parameter name while still keeping the old one around for backward compatibility.
You always have some concept of the API leak into the consumer. You either remember explicit names, or remember their positional pseudo-names 'first', 'second', 'third' etc. Unnamed arguments bring up more issues such as ordering consistency (see: PHP's many notorious cases), deprecation leaving 'holes' in argument lists, etc.
In Perl and many other languages we just provide a hash (aka dict) of key/value pairs. This lets the functions decide how to handle breaking changes which involves some annoying boilerplate but overall works fairly well. It's nicer with proper language support though.
The pattern in rust is to use enums and structs instead. Design patterns are always needed when the language doesn't support a given construct but once it's usefulness is established language designers incorporate to (the same or other) language.
The problem with named parameters as seen in Python is that then the name of the parameters become part of your APIs contract, making changes to your API harder. This has been remediated with patterns in Python 2 and explicit design in Python 3 by incorporating named only parameters so that you can't call them with both positional or named args.
>The problem with named parameters as seen in Python is that then the name of the parameters become part of your APIs contract, making changes to your API harder.
Huh? How are APIs with mere positional parameters easier to change and more transparent to the callers when changed?
This isn't quite what the OP is referring to. In Smalltalk, all arguments are named all the time, including positional arguments. Object methods aren't named separately from their parameters. They are only their set of named arguments, i.e.,
So say I have a BankAccount class. In Python I would write a method to transfer to another account
def transfer(this, to, amount): ...
and invoke it as
account.transfer(other_account, 500)
In Smalltalk I would write
transferTo: account amount: n [ ... ]
and invoke it as
account transferTo: other_account amount: 500.
So it's not quite named arguments. It's that there isn't a distinction between name of function, argument name, and positional parameters. It just doesn't map to the function notation typical from mathematics.
Named arguments in Smalltalk always seemed a bit clunky to me because the order of the keyword arguments still matters. You have to write the keywords, but you don't get any flexibility over their order, just like positional arguments. I suppose you get used to it, but it seems annoying and not very scalable to having messages with many optional arguments. Languages that support true named arguments don't have this restriction.
While Akka is great, it’s nowhere near as powerful as Erlang. Most notably, the inability to have a blocking actor yield control significantly changes the actor model (for the worse). Instead of being able to use messages like async function calls, you have to keep a lot of internal state managing all the actors you still haven’t gotten messages back from. This creates a lot of messiness and greatly complicates things. For example you can’t write something like:
Instead you would need to construct some sort of mutable dictionary that keeps track of when it got both messages and when it does, have the actor finally add the stuff together. It’s not always so bad, but when you have actors that are interacting with a lot of other actors and querying many different resources, it becomes a problem. Akka tries to mitigate this problem with the state change function become(), which allows you to build a FSM to handle state transitions. While this helps a lot, it’s not a panacea.
I think I read somewhere that true multi-tasking is in the works for the JVM, so this hopefully won’t always be a problem. As it stands, it’s probably the biggest limitation of the platform atm.
bank.move(new Amount(100), new FromAccount(checkingAccount), new ToAccount(savingsAccount));
If you are really trying, you will be able to convey enough information so that your fellow programmer. will understand what you are trying to accomplish. Unfortunately, in practice, even Java Experts will convey to you that this style is wrong, classes and objects are " too expensive", primitive values are fine, and that this is too verbose.*
*) Yes, Java is verbose; not trying to defend that. I am very happy about keyword arguments in Python, for example. But, as soon as you have classes, ctors, objects, you can use them to add rich semantics into your code. Works with functional languages and other, better typesystems, too.
Of course you can, but you can't simply hand-wave away the verbosity. What a language makes easy is usually more important than what it makes possible.
In Python, adding from=from_account, to=to_account to clarify a call is basically free; you don't have to change the callee and you don't have to change any other call sites.
I wouldn't write it either way. I would write something like bank.processTransfer(new Transfer(100, checkingAccount, savingsAccount)) where the transfer object is immutable.
Erlang is not running on top of the JVM but on top of its own VM (BEAM).
There have been some attempts to get Erlang running on the JVM but so far none that really made it worth adopting, and frankly I don't see how you could do it without losing everything that makes Erlang/OTP special.
Nah. It's about objects. Messaging is when you send a message off to the vasty deep and hope for a reply. A Smalltalk message is a function call that has to return.
Most of the issues around messaging come from waiting for answers. Hence callbacks, futures, async, retries, timeouts...
Rank 1: Polymorphism. You can "send" a message to an object. You don't know or care exactly what the object does. Other than the polymorphism, the "message" behaves just like a function call. It will definitely be handled by your target object, and it will definitely return instantly with a response. The compiler might even be able to inline this function call.
As seen in: Pretty much every OOP language.
Rank 2: Messages as first-class values. It's possible to write a function that forwards all incoming messages to another object, based on some logic. The caller does not know what will happen to the message. However, the message can be expected to be handled instantly, and it may respond instantly (if it does have a response).
As seen in: Objective-C, Smalltalk
Rank 3: Asynchronous messaging. When sending an outgoing message, it might be placed in a queue and handled at a later date. The message does not return a result instantly, so agents must have some other mechanism (such as including a "reply to" address) to be able to talk back and forth. From the perspective of the caller, it's a bit like putting a message in a bottle and watching it drift off to sea.
As seen in: Erlang, message queueing libraries.[1]
“When I use a word,” Humpty Dumpty said, in rather a scornful tone, “it means just what I choose it to mean—neither more nor less.” “The question is,” said Alice, “whether you can make words mean so many different things.” “The question is,” said Humpty Dumpty, “which is to be master—that’s all.”
> Rank 3: Asynchronous messaging. When sending an outgoing message, it might be placed in a queue
Arguments of a Smalltalk message can be "blocks" which are function-like objects. The receiver of a message carrying a block as argument can put the block into a list/queue and evaluate it any time in the future.
So that would seem to qualify as "asynchronous messaging" to me.
Blocks are closures, so they are bound to the sender's environment. If we accept the definition given above they'd thereby fail the "message in a bottle" test.
Not sure what exactly is the "message in a bottle" -test and why would we need one?
I think it is a limitation of "bottle-mail" that you can not easily reply to the message. Of course sometimes all you have is a bottle and a piece of paper and the sea around you :-)
There may be forms of messaging where the "medium" surrounding messages is more like an aether -- sorta like photons in space. And the addressing to objects super loose. I don't think it needs to be so strongly receiver-bound.
Synchronized requesting in Communicating Sequential Processes (CSP) [Hoare 1978] proceeds as follows: “Such communication occurs when one process names another as destination [to receive a request] and the second process names the first as source for [the request] ... [in order that providing the request] is delayed until the other process is ready [to receive the request].”
Synchronized requesting x with request r (i.e. x!r) can be implemented as follows using a 2-phase commit protocol: x.synchronize[Implements Provider ⟦provide ↦ r⟧] so that after x has received a synchronize message with parameter Implements Provider ⟦provide ↦ r⟧, x can get r from the parameter using a provide message (cf. [Knabe 1992]). Synchronized sending x a request r (i.e. x!r) can be algebraically reduced (which is a primary requirement of communication in the π-calculus [Milner 1993]) because x is provided with r without arbitration by Implements Provider ⟦provide ↦ r⟧.
Synchronized requesting (i.e. x!r) has the following significant costs in time, communication bandwidth, and robustness by comparison with unsynchronized Actor requesting (i.e. x.r):
1. The requester must wait for the receiver’s provide message in order to provide request r.
2. After receiving a synchronize message, the receiver must wait for the request r to be provided (meanwhile holding up processing of other requests).
3. Both the requester and receiver must be online concurrently for communication to take place.
Unsynchronized requesting (i.e. x.r) cannot in general be reduced using an algebraic equation as in [Milner 1993] because in general, the request must go through arbitration in order to be received. Although algebraic reductions may be elegant mathematics, synchronized requesting is not widely used in large software systems because it is slower, uses more communication bandwidth, and is less robust than asynchronous requesting (especially for IoT).
According to [Milner 1993]:
“An important task is to compare it {π-calculus based on algebraic reduction using synchronized requesting} with Hewitt's Actors; there are definite points of agreement where we have followed the intuition of Actors and also some subtle differences, such as the treatment of names {i.e., request providers}. More generally, the π-calculus is a formal calculus, while the Actors model, in spirit closer to the approach of physics, sets out to identify the laws {i.e. [Hewitt and Baker 1977] expanded into the uniquely categorical axiomatization in this article} which govern the primitive concepts of interaction.”
I’ve tried applying Actors to my C++ projects and always hit 2 issues in Actor model:
1) the need for synchronous access. As long as a single thread is accessing each Actor sequentially, then there is no issue with locking an Actor for synchronous access (barring deadlocks). I feel dirty locking Actors, but it solves many practical problems.
2) sharing resources requires a language which understands locks, since even Pony’s reference capabilities doesn’t solve real world performance issues. Ideally, each shared resource would have a paired lock whose usage the compiler verifies.
Both issues deviate from Actor model purity since they require locks, but I cannot resolve real world performance problems without them.
I feel woefully inadequate among present company, but nonetheless offer my 2 cents:
There is a fundamental performance tradeoff between sequential vs concurrent computation, because concurrency always requires some form of communication. Locks are difficult to reason about, but allow for shared resources. I've found the actor model to be so useful because it allows me to trade the ability to share resources for a more intuitive form of communication, message passing.
Regarding your 1st point:
Each actor is itself a sequential computer. You can't share an actor between threads any more than you could share a Turing Machine between threads. When designing actor systems I imagine a 1:1 mapping between threads and processors. The concurrency is happening in userspace.
Regarding your 2nd point:
It's my feeling that current actor model implementations are only suited to a narrow range of problems characterized by massive concurrency, a lack of shared state, and flexible time requirements. If any of those aspects are missing, I've learned to look elsewhere for a solution.
>Each actor has an address. In various implementations an address can be a direct physical address (e.g. MAC address of the NIC), email, memory address, some id and so on. Multiple actors can have the same address, and one actor can have multiple addresses. There is a many-to-many relationship here. Address is not a unique identifier of the actor. Actors don’t have an identity, only addresses. So, when we step back and look at our conceptual Actor Model, we can see and use only addresses. We can not tell whether we have one actor or multiple ones even if we have one address, because it can be a proxy for the group of actors. All we can do with an address is send it a message. Address represents capability in the Actor Model. Mapping of addresses and actors is not part of the conceptual Actor Model although it is a feature of implementations.
> A Smalltalk message is a function call that has to return.
Yes, but, that does not mean "async messaging" is not supported.
A Smalltalk "message" can return "nil" immediately in other words return nothing. A message-argument can be a "block" which is an executable function-like object, in fact very close to JavaScript function-closures.
So the recipient of a message carrying a block can at any time in the future ask the block to execute itself, with arguments, and thus produce async "replies", again much like with JavaScript "callbacks".
The arguments of a block can be blocks as well thus providing multi-way message passing.
What Smalltalk does not support out of the box is Distributed Programming, i.e. multiple machines communicating and working together passing messages among themselves. You will have to create your own servers and communications protocols to do that.
The other bit of that comment is also very insightful:
"I would say that a system that allowed other metathings to be done
in the ordinary course of programming (like changing what inheritance
means, or what is an instance) is a bad design. (I believe that systems
should allow these things, but the design should be such that there are
clear fences that have to be crossed when serious extensions are made.)"
Below is the fundamental axiom for Actor events (i.e. communications:
∀[S:ActorSystem, P predicateOn Event<S>] // For every ActorSystem S and every predicate P on events of S
(∀[x:Primordial<S>] P[Creation[x]] // If P holds for the creation event of every primordial of S
⋀ ∀[e:FromOutside<S>] P[e] // and P holds for every event from outside S
⋀ ∀[e:Event<S>, e1:ImmediatelyFollows<e>] // and for every event of S
P[e]⇒P[e1]) // if P holds for the event, then P holds for each immediately following event
⇒ ∀[e:Event<S>] P[e] // then P holds for every event of S
Objective-Smalltalk ( http://objective.st/ ) is a newish smalltalk that was very much inspired by this very message (no pun intended). And by Mary Shaw's work on software architecture, and, and...
So what might make a good metasystem? The ones we have are usually too specific, too concrete, the metasystem for this particular language, sort of like having a "types" rooted in concrete machine types (cough, C, cough). Smalltalk solved this for the type-system, with the root being something abstract, "Object". If you apply the same idea of creating a hierarchy to the metasystem(s), you come up with something along software-archtitectural ideas of component (procedures, methods, objects, filters, programs) and connector (calls, message-sends, pipes, variable access, ...).
And it turns out that this gives you parsimony, great power, and pretty darn good security of meaning.
A great visual example of messaging / sharing between two <s>apps</s> tools is in "Alan Kay's tribute to Ted Nelson at "intertwingled" fest": https://youtu.be/AnrlSqtpOkw?t=607
Passing a single, immutable object as a parameter to a method, and replacing several native parameters, is a common "what-if" comment I make in code reviews.
It's not 100% the intention of Smalltalk messaging, but it's in the spirit of "objects as messages".
It's about messaging and also loose coupling, referentiality, resources etc. It's about being able to reference by some reference(UUID) rather than by values.
This idea gives rise to contexts.
It's the difference between graphs and trees.
It's the difference between linear logic and classical logic.
In some sense, heap allocated objects work like this. The main problem is that you can't relocate objects.
As a dev, I grok that smalltalk is all about messaging but I find that it bores me to discuss the trivialities of sportsball with marketing or business people.
Many people here have read the same case (OO as intended by AK and Smalltalk being not about Objects but about messaging) made in different outlets from Alan Kay over the years (I've read it over 10 times in various forms, including 2-3 videos).
It has also been posted on HN for over 10 times, and a few times Alan Kay himself was commenting here too.
So it's not like everybody needs to read this particular "TFA" to jump into the discussion, or that it's that bad if they missed some specific reference you've made to the content of this particular article...
Keep in mind, this `mybool` could be anything! It just so happens that there are two runtime objects, `true` and `false`, that satisfy the “Boolean interface” (consisting of messages ifTrue and ifFalse) the way you’d expect—with `true` running any closure passed in an ifTrue message, and `false` running any closure passed in an ifFalse message. (For those of you who know combinatory logic, these objects are essentially the K and SK combinators.)
But consider that you can create your own object that implements this interface—with whatever arbitrary logic you like—and substitute it for a Boolean as the value of `mybool`, and no code will be able to tell the difference.
This is what is meant by “computation as messaging.” Branching is a request to an opaque “branch-evaluating object.” At each step of computation, you’re not getting the runtime to compute—you’re getting your objects to compute. The objects are the computers, and the messages are the instructions to those computers!