Why are there no return statements in Objective-S?
blog.metaobject.com
blog.metaobject.com
Rust is as expression-oriented as they get, and it does feature return statements (well, expressions: they have the empty type, implicitly convertible to any other type). I don’t see how those two are supposed to be exclusive. In fact, they very much go hand in hand: ‘expression-oriented’ is just another name for functional programming, and a return statement is just a limited (non-reified) form of continuations, a well-known concept under this paradigm. I don’t know what’s to write about here, really.
Also, it’s pretty ironic to see the article support its claims by a quote from Guy Steele disparaging functional programming, immediately followed by an example like
#Blinker{ #seconds: 1, #active: true} → ref:gpio:17.
which looks like mapping over a generator in all but name.I love objects and talking about them as living creatures. :)
They’re sort of like functions that include a long set of parameters.
The one thing I wouldn’t know how to do / name in functional pure functional programming is an incremental parser.
My sense is that would probably have to pass an “object” like parameter like in C that gets updated by the function. What am I missing?
factors of hundreds.
What is actually there is a connection expression, connecting two objects.
The first object is a data source, generating values at a fixed rate.
The second object is a reference to the object identified by the polymorphic identifier "gpio:17". What is that? Pin 17 on the Raspberry Pi GPIO connector. But "gpio:17" is the current value. By taking the ref:, I can talk about the variable itself, which I can connect as a sink of a dataflow. In fact, I can connect any variable as the sink to a dataflow.
So the following:
#Blinker{ #seconds: 1, #active: true} → ref:file:/tmp/blinker.txt.
would change the contents of the file in question every second. It would not append to the file, that would be accomplished by getting the stream: #Blinker{ #seconds: 1, #active: true} → ref:file:/tmp/blinker.txt asStream.
Or you could just use stdout: #Blinker{ #seconds: 1, #active: true} → stdout.
But coming back to ref:gpio:17, to me this seems about as effectful as possible, in my test setup that literally lights up an LED, so about as non-functional as you can get.I assumed active was to turn the GPIO high, given that the code was supposed to implement the same logic.
edit: yeah ok I see it now: describe the actual relationship, make that explicit, and instead keep the procedural code that maintains it as a hidden implementation detail.
If it could be explained better, I am all ears!
Not a huge deal though :)
Having active elements activate immediately is a good choice for simple examples like the one shown. For larger constructions, you typically want to have built the whole thing before starting it up.
(And by the way: pre-sliced bread sucks.)
"I am not seeing anything here that would differentiate this from x86 assembly"
¯\_(ツ)_/¯
Anyway, if you insist on mapping something new in computing onto something you already know, you will pretty much always find some way of accomplishing this. While it might seem clever, it really just reiterates Turing-completeness, and so is not particularly interesting.
The question is whether your mapping is appropriate (it's not) or straightforward (also not).
Of course, this is a problem, because to understand something, you typically first have to map it onto something you already understand, but the key is to not stop there.
As to "something new": no, software architecture is not new. Not new at all. The seminal book is from 1996: Software Architecture: Perspectives on an Emerging Discipline, by Mary Shaw and David Garlan.
https://www.amazon.com/Software-Architecture-Perspectives-Em...
The paper trail is much older.
There is an entire discipline behind this, and in this discipline, FP is a small corner of the small corner that is the call/return architectural style, of all the myriad of architectural styles available. You can argue that this whole discipline of computer science with conferences, many thousands of peer-reviewed articles, many books etc. is all just BS, but I think you'd have a hard time arguing that case. And it would be you who'd have to make the case, because the discipline is well-established.
All Objective-S does is to say "hey, let's take this talk of connectors and components seriously and program with them". It's also not the first language to do this, there was ArchJava. Alas, this was an extension of Java with architectural constructs such as connectors, which is the wrong way around: connectors are more general.
Objects don’t “do” anything, they are simple passive data structures that are acted on by procedures. A certain set of procedures with privileged call syntax and visibility into the data structure are called its “methods”, that’s all the magic. The sooner you accept this, the better your designs are going to be.
(1) Edit: I meant here that you can't design real-world systems that way in OOP languages. I left that out because I considered it obvious from context, but it seems like it wasn't. I didn't mean to disparage any actual message-passing systems. My claim here is that OOP, as commonly understood and implemented by Alan Kay himself in the form of Smalltalk, is not about message-passing. Erlang is, but that's a different topic.
Because you don’t, dynamic dispatch means you don’t know and are not supposed to care how the object handles or responds to the message.
That is maybe more flagrant in Self.
2. Message passing is an ideal, an ethos, the implementation (or the interface) can get in the way of that. That the messages are implemented as dynamically dispatched function calls… would be an implementation detail. Though with far ranging implications of course (especially in that the response is direct and synchronous rather than an other message as it would be in, say, Erlang).
The mention of Self was not innocent, though it does use synchronously dynamic dispatch, a message might invoke a block, or just set or retrieve a value. If you’re asserting that it’s all « calling a function » then you’re basically just reductio at absurdum-ing everything to calling functions which… sure? You can model and reduce everything to lambda calculus.
It can't both be only an implementation detail and also have far reaching consequences. (It's not an implementation detail.)
> You can model and reduce everything to lambda calculus.
You actually can't, that's precisely my point. Lambda calculus is inherently "synchronous". You can't capture the full semantics of actual message passing with lambda calculus, you need a process calculus (like pi calculus) for that. (In case you're confused, this has nothing to do with Turing completeness, it's about modeling the behavior of concurrent systems. It's a really interesting subject.)
The point is, lambda calculus is sufficient to model OOP method calls, including everything in Smalltalk and Self. It's not sufficient to model the semantics of Erlang messages.
I don't think "You don't know the implementation" implies you're not calling a function - otherwise we'd have to say that when you use a shared library like OpenSSL, you don't "call" SSL_CTX_init(), you pass a message to the OpenSSL library, which might be 1.1.0 or 1.1.1 or even BoringSSL. But nobody would say that - it's obviously a function call.
(I suppose you could defensibly argue that a system call is message passing, at least on some OSes, but that's about it.)
Erlang disagrees. BEAM processes and its implementation of a mailbox is probably the purest interpretation of Alan Kay's OOP, and you can indeed design and understand real-world systems using the actor model as implemented in the BEAM VM.
In practice it's very simple to grasp, it's concurrent out of the box yet you never have to think outside the single thread.
(Also, personal opinion: The "purest intepretation" of what Alan Kay had in mind in 1969 is Smalltalk. That's almost an objective fact: if he had something else in mind, he would have done it. I know that he says he had something like Erlang in mind from the very beginning, but to the best of my knowledge, those claims are retroactive.)
You’re totally missing the point. Erlang is more “message passing” than any OOP language I’ve ever used. It honestly just sounds like you’ve never used erlang since you immediately referenced actors.
My point is that OOP is not about message passing. You say that Erlang is more about message passing than OOP languages are, aren't we exactly in agreement here?
Also, it was the post I responded to that referenced actors, I'm not jumping to anything.
However (1) synchronous messaging IS messaging (2) we are moving to asynchrony.
So the original definition of OOP is still the overarching theme of programming today, and it's quite helpful because those same mainstream messages are evolving to asynchrony, now as IO and cross-process and cross-machine communication has become the norm.
Your limited mental model of objects just being structs with procedures works in the 90s, but it'll be detrimental when you design any system today, because you often need to be ready about asynchrony, redundancy, resiliency and non-immediate results at your module's boundaries.
Basically you've focused too much on a few trees and you've missed the forest. But those trees are just happenstance, and the forest (the entire "OOP" with its implementation hiding, polymorphism and message passing) is the important paradigm and it's what everything is moving towards.
At the low level, you can still think about objects as just Abstract Data Types. But that doesn't give you working systems at scale. It gives you just a set of data primitives to get started with.
To me, it seems like you're arguing that having a less accurate (I'm sorry, less "limited") understanding of actual language semantics (that it's just "message passing") somehow helps you design distributed systems. In my experience, the opposite tends to be the case.
And once again, synchronous message passing doesn't mean it's not message passing.
You just gotta get your terms and frame right.
that happens to support mailboxes, one-way messaging, and
ability to update processes. Erlang programmers can be
inefficient because Erlang itself does not directly
support the Actor abstraction.
theory Actors. Lambda expression and Turing Machine differ in
that each is defined as a particular kind of machine.
Actors can perform computations that cannot be implemented
by a nondeterministic Turing Machine. Also, an Actor
application can be hundreds of times faster than any lambda
calculus implementation.
A programming language implementation is also a particular
kind of machine. For example Smalltalk-71 was a byte code
interpreter machine not based on Actors.
See the following:
Besides concurrency benefits of the actor model, message passing implicitly supports adaptive behavior. A message is, by definition, interpreted by the receiving actor. You can hot swap in Erlang precisely for this reason.
"The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be. Think of the internet - to live, it (a) has to allow many different kinds of ideas and realizations that are beyond any single standard and (b) to allow varying degrees of safe interoperability between these ideas.
"If you focus on just messaging - and realize that a good metasystem can late bind the various 2nd level architectures used in objects - then much of the language-, UI-, and OS based discussions on this thread are really quite moot."
http://wiki.c2.com/?AlanKayOnMessaging
So, it's this 'late binding' business -- the "interstitial" -- that is really interesting, powerful, and conceptually key to Kay's vision of organic systems. And this is basically reducible to one of the most powerful architectural concepts in software: indirection.
https://en.wikipedia.org/wiki/Fundamental_theorem_of_softwar...
Now curious as to which of these two first grasped the beauty of indirection in structures.
It's how they work, however. The fact these messages tend to be synchronous in languages like Java which are mostly about intra-process communication is a matter of optimization.
But look at Erlang. That's probably the most pure OOP language we have, and hence why it scales so well the way Kay predicted OOP would.
Also all our "web APIs" are in effect OOP.
> Objects don’t “do” anything, they are simple passive data structures that are acted on by procedures.
Those procedures are an inseparable part of the objects, which is one of the first thing you learn about OOP...
> I meant here that you can't design real-world systems that way in OOP languages.
You need a relatively small step from OOP to actors. Sure some help from the language runtime would help to keep the model clean.
But let's say that mainstream OOP languages take SOME but not ALL principles of OOP and those are the principles they had use for in their context. This is how it should be. But we're slowly realizing the need to have real OOP in the mainstream now, which is about message passing.
But we're slowly evolving towards a concurrent async message passing models, and they're slowly being integrated into mainstream OOP languages.
In effect what we call by various names like SOA, microservices, EDA, actors, and so on, is basically OOP at scale.
It took me days of banging my head against the wall to understand that in the object world messages were actually synchronous message calls and that the sender couldn't do anything until the object method returned.
In most OOP languages. Out of many different languages which implement various interpretations of how OO should work, I found Io to be the purest, and for some odd reasons the most practical, implementation of object orientation.
In Io, you get two primitives: objects with slots and messages (which are also objects, of course). There's literally nothing else in the language. There's a bit of syntactic sugar, or rather, there can be a bit of syntactic sugar if you want, because the parser is an object you can customize on runtime, but otherwise it's just objects and messages. Message contents can be passed by name (lazily), and you can transform messages in any way you want before they get send. Or after. Taken together, you get a run-time equivalent of Lisp macros (Io is also homoiconic, btw). Add to it the fact you can reify interpreter state as first-class objects, and you get incredibly expressive language based on extremely small set of concepts. I think allowing messages to be first-class values able to exist without specyfing a target is the key here. It's also what Erlang does, with the difference being the sending and receiving messages in Erlang is hardcoded in BEAM, while it's 100% customizable in Io.
My favourite example of the power of Io:
someObj someMsg(some, args)
this is a simple message send. It will try to look up `someMsg` slot in the target `someObj`, will traverse the prototype chain if needed, and will activate the content of the slot if it's a block of code. someObj @(someMsg(some, args)) # outer parens optional
this also a simple message send, where the message name is `@`. `@` is defined in `Object`, root of all prototype chains, and it: suspends evaluation of `someMsg`, saves a pair of `someObject`+`someMsg` in a queue in a Scheduler object and returns immediately. In other words, this time the message is delivered asynchronously, and executed the next time some Coroutine (including main) yields. It's not a built-in primitive, you can implement `@` yourself pretty easily....anyway, when I hear about message passing, and think about it being "done right" it's "Io and Erlang" that come to mind. Very different implementations, but they both work exceptionally well.
The compiler can then warn you if you return a value when you weren't meant to, or if you forgot to return one when you were. And the reader can tell at a glance that this value is important.
Another issue is that there are actually 2 functions of the return statement: (1) it sends a result and (2) it terminates the method/function/whatever. In call/return, those are the same, but more generally they are not, and Objective-S wants to make it easy to not default to call/return semantics.
And that's difficult, because we are so incredibly used to call/return semantics that it's often hard to see that another way is even possible, let alone practical. So in the design, I need to very deliberately push against what seems comfortable and familiar.
Which can lead to insights or nonsense.
We'll see how it turns out.
[1] https://github.com/CodeNarc/CodeNarc/issues/450 [2] https://stackoverflow.com/questions/24070544/suppressing-imp... [3] https://nug.xojo.narkive.com/GvvcWglM/bug-command-keys-are-a...
For our compiler, for certain types it fails to warn that the return variable is unassigned, hence bugs occur.
This is a statically typed language, it's just an unfortunate interaction between compiler magic and the code that checks for unassigned return variables.
The problem is how to encode early returns in that context.
That doesn't mean doing it is a good idea. Every time you do, you sacrifice a bit more.
"Last line of a function" is honestly a very blatant and obvious way of encoding a returning point (and hence returning expression in an expr-oriented language).
You literally can't miss which line is the last line, UNLESS you're simply habituated into seeing the keyword "return".
You said it's about communicating intent, not about matching current habits. Those are completely different things.
For example, someone who knows Russian and is learning English, so they make mistakes in English. That doesn't mean English is a poor language for communication, does it?
Case in point, do you actually type "return" in say Java, for a void method, when that return is your very last line of the method? Yes, no?
If no, then you didn't need to communicate "it's returning here" with greater intent. If yes, then I'll accept your point about you finding no return ambiguous. But still, most people don't.
And, even if it were the last line of the function, you are still not communicating clearly whether the function is meant to return a value at all or not. This both hurst readability, and makes it easier to accidentally introduce bugs.
Because the "does it return value or not" question doesn't sit in expression languages. They always return value.
If the returned value is a single line, it's easy to see what is being returned. If it's a 10-lines if/else, and you don't remember the function's return type, and you're not sure how many layers of control flow you're at, etc, it gets a little ambiguous.
You can mitigate that with good coding practice, though. There could maybe even be turned into lints.
One way to think about it is how assigning subexpressions to a descriptively named variable helps reader comprehension, instead of nesting everything into one big expression. "Return" fulfills a similar role.
(In some imperative languages, the return value is defined by assigning it to the name of the function — or rather, to an implicitly declared variable having the same name as the function. While that design has some problems of its own (mutability, default values, naming), it exemplifies the analogy with descriptive variables noted above.)
What we currently call general purpose languages are actually domain specific languages for the domain of algorithms.“
What is this supposed to mean? Perhaps I’m being dense, but it looks like nonsense to me.
It would have seemed like nonsense to me a year or two ago.
The issue is incommensurability (https://en.wikipedia.org/wiki/Paradigm_shift#Incommensurabil... ), though I find the idea that communication or translation is impossible a bit too strong. But it definitely is very difficult.
As a start, think of what ALGOL stands for: ALGOrithimic Language ( https://en.wikipedia.org/wiki/ALGOL ). Now think which current mainstream language is not derived from ALGOL. There are some variations, but they are fairly minor. OO languages attach the procedures to the data structures, and call the result methods and object respectively. FP languages insist procedures must not have side-effects and call them functions. And so on.
(And once I remembered what ALGOL stood for, I thought that what I had thought of as a deep insight was so trivial that obviously everyone already knew it. I guess not :) ).
Anyway, my first semi-coherent written account of this is here: https://2020.programming-conference.org/details/salon-2020-p...
In short: the first thing we used our computers for was computing answers. For this functions/procedures are well suited, and so we also used procedures (sub-routines) as our structuring mechanism. The problems we are solving with our computers have changed dramatically, but our structuring mechanism has remained essentially the same.
Another short: software architecture defines many different kinds of connectors and components. The procedure/function call is just one of them, but that's the one that is directly supported by the majority of our programming languages. So if we need a different architectural style, we have to cobble it together out of procedure calls.
Subdividing a big problem into a set of smaller problems is not just some specific way of programming, it's not even a specific way of factoring a system. It's universal, as in the entire Universe essentially is factored this way.
We have areas of "widening" cohesion and coupling (implementation) and areas of "narrowing" cohesion and coupling (interfaces, incl. argument/return types). These occur both horizontally and vertically as we move towards smaller and smaller problems to solve.
Your actual CPU works this way, itself.
So I'm very curious what we're talking about and why the dismissive attitude towards procedures i.e. "cobble it together out of procedure calls".
In these conversations usually people just bring up something else that's also a procedure, but we call it something else, like "message passing to object methods".
That's a useful metaphor at a higher level for sure. But let's not forget a method is just a procedure that runs and does things like any other procedure.
-----------
Thanks for the great questions! I am particularly delighted by the slight incredulity expressed as to whether it's actually possible for there to be anything else.
This has been my thesis for a while, that the call/return style has become so incredibly dominant that it is now largely synonymous with programming itself and thus something that is programming but "not call/return" seems self-contradictory and non-sensical, because if programming = call/return, then that would become "programming but not programming". Clearly illogical.
However, there are actually quite a few other connectors, and call/return is really just a small but important corner of the "connector space". See for example Towards a Taxonomy of Software Connectors:
https://dl.acm.org/doi/10.1145/337180.337201
https://www.ics.uci.edu/~andre/ics223w2006/mehtamedvidovicphadke.pdf
But any book on software architecture will do, for example Software Architecture, Perspectives on an Emerging Discipline by Garlan and Shaw: https://www.amazon.com/Software-Architecture-Perspectives-Emerging-Discipline/dp/0131829572
Objective-S concentrates on 3 architectural styles and their corresponding connectors, in addition to broadening call/return to full-bore messaging:1. Dataflow: (Unix) Pipes and Filters[1], dataflow constraints[2]
2. Data access: (In-Process) REST[3], Polymorphic Identifiers[4], Storage Combinators[4]
3. Implicit invocation: Notification Protocols[6]
Most of these papers can be accessed despite the paywall from the the Objective-S publications page: http://objective.st/Publications/ (that's an exception allowed by the ACM).
> let's not forget a method is just a procedure
Exactly! I had a good laugh when I saw Rust described as a "multi-paradigm"vprogramming language in an official Mozilla presentation, because it supports "imperative, functional and object-oriented" programming. These are all (very) slight variations of call/return.
> Subdividing a big problem into a set of smaller problems is ... universal
Absolutely! Recursive decomposition is fairly universal (and powerful). However procedural recursive decomposition is not. Except in programming, where we are only allowed to abstract using procedures. Which is both extremely weird, once you think about it, but at the same time so universal that it is extremely hard to notice it as a limitation, and even harder to think of alternatives.
The first time I really noticed this was when looking at the OCaml Lwt user-level threading library (for I/O), which had an innocuous looking passage describing CPU-bound threads as a special case. Really? I mean, yes, but no.
Anyway, I tried writing this up here, but it's just way too long, so I'll just write another blog post. Once you become aware of this issue of using call/return where it isn't really appropriate, you find it everywhere, and you notice that a LOT of the "accidental" complexity we currently suffer in development appears to be attributable to this issue.
"Code seems "large" and "complicated" for what it does" -- Alan Kay (https://youtu.be/ubaX1Smg6pY?t=113)
"One of the complaints I have is that the teams doing products that you see are far larger than they should be. And that's a failure of architecture" -- Eric Schmidt https://youtu.be/hcRxFRgNpns?t=3591
[1] https://dl.acm.org/doi/10.1145/3359619.3359748
[2] https://dl.acm.org/doi/10.1145/2889443.2889456?cid=813164912...
[3] https://link.springer.com/chapter/10.1007%2F978-1-4614-9299-...
[4] https://dl.acm.org/doi/10.1145/2508168.2508169?cid=813164912...
[5] https://dl.acm.org/doi/10.1145/3359591.3359729?cid=813164912...
[6] https://blog.metaobject.com/2018/04/notification-protocols.h...
I did ask the question on Twitter. :)
Not entireley convinced but I’m certainly looking forward to using it.
One more thing—how would you do error handling?
Thinking out loud here: it would be nice if NSObject had a method `isError` that went something like `self isKindOfClass:[NSError class]` so that you could return error in a more clean way. Wonder, what would mpw do?
Another question—how to MPW multiple return items? (My guess is that the return items would be mutable? Like a mutable string. Could work with numbers if you had such a thing as NSMutableNumber.)
I’m sort of looking for what 2nd order and systemics effects `mpw` might expect.
To me, Swift is a bit more annoying with `if let`. Seems like it might provide a benefit, but it’s not so compelling to me. The compile times are so much slower so as to turn me off completely.
This could be solved by pattern matching as well, but that might not be a good fit for any language.
request it has received. Instead, the Actor sends a 1-way
message to the customer Actor that it received with the
request. Actors operate concurrently, which is different
from the object-oriented programming beginning with Simula.
"With those more general semantics, "^" might also be used to send back results to the sender of an asynchronous message, which is obviously quite different from a "return"."
of the request. Instead, the response goes to the customer
that was received along with the request. For example, tail
recursion makes of the ability to specific a customer along
with a request.
An Actor never knows where the request came from. Of
course, the request my be signed to designate accountability
for the request.
b) sender / "customer" / whatever.
Whoever gets marked as the "receiver of results".
From TFA: "I've already taken that and generalised it to mean "send result", using it in filter definitions, where "^" means "send a result to the next filter in the pipeline"
The proofs of correctness for expression programming languages are a special case of the proofs for Actor programming languages.
See the following: