The setup
I'll take a different approach than the other answers.
What if we actually allow statements to be a new kind of expression, different from the others? What if that new kind of expression even had its own operations? Let's find out!
Let's say that in our language we actually "embed" the statements into the expressions like this.
A statement can be thought of as an "action." Two actions can be combined to form another action. For example, if you have an action x
and an action y
, you can use a sequencing operator (call it ;;
) to combine them into a new action. This new action will first do the x
action and then do the y
action.
Say we want to print 1
and then print 2
. These are two actions. You might write this print(1) ;; print(2)
. Of course, you can come up with much better syntax, this is just for illustration. ;;
is a binary operation that takes two actions and gives you back another action (the "combination" of the two argument actions).
While loops
Alright, now what is a loop in this language? Well, let's start with a while
loop. We can actually think of a while
loop as a function that takes a Boolean expression (the conditional) and an action (the body) and returns an action. The action it returns is the action that repeats the while loop body action while the conditional is true.
while
actually doesn't need to be a keyword here and, in fact, there are ways you could implement it inside standard library of the language.
You could write it like this (again using a made-up syntax purely for illustration; there are much better ones):
while(x > 5,
(print(x) ;; x := x-1))
It looks like I've just written a while loop in a super weird way. And that is technically true. However, we've also gained something new. Actions are now first-class values in our language!
We can actually pass around the loop body I've written above. Say you want to abstract the notion of "looping 15 times." You can write a function
repeat15 =
fun(a) =>
let i := 0
in
while(i < 15, (a ;; i := i+1))
Now you could write
repeat15(print("*"))
and you'll print out 15 *
s.
Also, if we wanted to, we could distinguish actions from "regular values" (like integers or strings) at a type level. We can do this by creating an Action<T>
type for actions that produce a value of type T
. We can distinguish between a string value we already have in our hand from an action that produces a string value. And, furthermore, we can pass both of those kinds of things around at will.
But what is the difference between an Action<Int>
and an Int
? Well, it's the exact same as the difference between /bin/ls
and a list of files (paraphrasing a quote from Shachaf).
For loops
A for
loop (or a foreach
loop) can be thought of as a function that takes in a list and a "parameterized action" (a function that gives you back an action). It returns an action that does this: it will take each item of the list in turn and give it to the "parametrized action."
For instance,
for([1,2,3,4], fun(x) => print(x))
would print 1 2 3 4
(writing spaces for newlines).
So, what does a for
loop return here? It returns an action! It doesn't return any particular value produced by the loop. It returns an action that actually performs the loop itself.
I mentioned that we can also have a version of ;;
that passes a result from the first action to the second. Here's what that could look like. Call that operator -->
:
getLine() --> (fun(x) => print(length(x)))
This would read in a line from standard input and print its length.
Note that ;;
is just a special case of this:
a ;; b
is the same as
a --> (fun(_) => b)
As I've mentioned, you can come up with a better syntax. In fact, there already are better syntaxes. The reason is that these ideas already exist.
And that leads me directly to...
The punchline
What I've just described is a particular case of the concept of a monad (in the form in which it appears in programming languages).
Now, there is some debate about how people should learn about monads. Ultimately, I can only provide part of the answer for this.
I've only described part of one specific monad here. I believe to learn them properly you must experiment with a variety of them. For example, look at the list monad, the reader monad and the proxy monad. There are plenty more, but that is some variety to start with. Think about how the "bind" operation works for each of them works, in each particular case.
In the context of this question, the list monad in particular would be useful to look at. Also, my answer to a question here talks about the Maybe
and Either
monads in a somewhat similar style to this answer.
I also did not describe the mathematical laws they must obey (though there is a relatively intuitive way to look at these laws in terms of actions).
I've only scratched the surface of a particular monad here, but hopefully this answer can provide some useful ideas.
The particular monad I've described is roughly like Haskell's IO
monad, though I've left all calls to writeIORef
, readIORef
and newIORef
implicit. Also, the let-in
in the repeat15
should really be a call to -->
. I've simplified that for the purposes of demonstration. There are many other monads, as well. It remains the case that -->
is the basic "action combining" operation. In Haskell, this is called >>=
. Likewise, the Haskell name for ;;
is >>
. There's also an operation that will take a "regular value" and give you an action that simply produces that value without doing anything else. This is sometimes called (in Haskell) pure
or (confusingly) return
. It's also sometimes called "unit."
None
, unless youBreak
with some value. $\endgroup$