A New Idea For Functions
Introduction
Man, I’ve had this idea in my head for more than a month now (luckily I wrote it down, too), waiting until the day I wrote this post. I didn’t write it because I was busy with the move and new job, but now things are finally settling down!
Here’s the thing: When you really dig into it, proper object-oriented programming and functional programming are pretty similar. The biggest difference is that object-oriented programming likes to use encapsulation to hide the real data behind facades of objects – requiring you to define methods attached to the type that know about private details – while functional programming is quite up front about it all – making it so that you generally get switch-like structures (but WAY better) that allow you write one function to handle all of the different types.
A side-effect of this has been two different ways to call functions: as methods and as stand-alone functions.
object.methodCall() // Vs functionCall(object)
Actually, the second kind could be procedural or functional in style. It’s impossible to tell without more arguments.
Due to functional programming loving to compose functions together, it likes to have the “subject” of the function as the last argument so that the function could be curried or partially applied to a base function that could be composed with other functions that work on the same type or output type. It’s really cool and really handy. But with procedural style, the tendency is to put the “subject” as the first argument in order to show its prominence.
object.methodCall(argument) // OO style functionCall(object, argument) // procedural style functionCall(argument, object) // functional style
Interestingly, Python methods are actually explicitly defined in the procedural function style, but are called in the OO style (usually). This sparked an idea within me, which, with knowing how so many other languages do functions, guided me to today’s idea. What if you could define a function in a single way, but use it in any of the three ways listed above?
Ground Rules
First, I’d like to say that I would prefer to use this system without the need for parentheses or commas, but for the ease of reading, I’m going to use them. That is, I will until we get to the section about the new possibilities we open up when we get rid of them.
So, for this system to get any kind of traction with people, it has to be simple, easy (those two are different), and not very distracting.
Defining the Function
Defining the function is almost exactly the same as it is otherwise, with one or two tiny differences: you have to mark the subject of the function somehow (my recommendation is putting a period at the end of the name of the parameter). And that subject has to be the first parameter in the list. Like in Python, the subject has to be explicitly added to the parameter list, even if it’s a method. So, to Pythonistas, this is not a change. To users of other OO languages, including what is usually implicit may be a little jarring.
The reason why you have to explicitly mark the subject is because some functions do not lend themselves to being used with a subject. Take maximum()
and minimum()
, for example. All of their parameters carry equal weight. So, something explicit has to be done.
An interesting idea that builds upon this is wondering how to use the implicit this
variable so common in OO languages. Well, instead of using an implicit this
(or self
in Python, which, sadly, cannot be used implicitly), we replace the entire parameter name with the period, and this
is simply shortened to a period for the whole call, making it nearly invisible but still explicitly showing that it’s being used (and if you know the Zen of Python, you know that Explicit is Better Than Implicit).
This is the same idea as in math, where you simply use a dot – or even just mash the two operands together – to show multiplication. It’s so common to use multiplication that shortening how it’s written makes it that much easier to use. In this same way, the subject is so commonly used that having a shortened way of using it makes writing the code that much easier.
This will also have some really cool effects when it comes to writing lambdas, as we’ll see later on.
Calling the Function
Now we get to the cool part. With such a simple change to defining the function, there has to be some convoluted way of calling the function, right? Wrong. Let’s take a look.
Object-Oriented Style
There is actually absolutely no change to calling the code in an OO fashion. It’s still just object.methodCall(arguments)
. See? It’s seriously simple. But I’m getting ahead of myself; we still have two more to go.
Procedural Style
This is where we actually do something new, but again, super simple. All we have to do is put a period at the end of whatever we’re passing in: functionCall(object., arguments)
But what if the subject is some calculated value? Easy. Just wrap the calculation in parentheses, then put the dot: functionCall((a + b + c)., arguments)
Functional Style
Okay, this one is weird. It doesn’t work the way you would think. I’ve thought about this quite a bit, and when it comes to the functional style, you only really do it because of composability of functions. So, for this style, it actually looks like this: functionCall(arguments)(object.)
.
If the subject doesn’t prefix the function name and isn’t the first argument in the list, the compiler assumes you’re doing it in order to compose functions, so it creates a partial function, leaving the subject out. This makes clears away messy calls to things like partial()
when you’re trying to compose functions together.
True, in some functional languages, functions are designed to curry, so this would be automatic anyway, but we’re trying to work in a nice hybrid language that makes both styles equally viable, and including automatic currying could mess with the “vibe”.
Because of the assumption that you’re setting up the function for composition (or maybe even just making a new function with all the arguments preset to be used later), you’re not likely to actually do the function call as shown above. Assuming >>
is a left-right composition operator, it’s expected to look more like this:
newFunc = functionCall(arguments) >> otherFunction(arguments) >> anotherFunction(arguments) … newFunc(object.) // or object.newFunc()
By the way, if you’re going to use the composed functions immediately, this is more simply done in the OO style like this:
object.functionCall(arguments).otherFunction(arguments).anotherFunction(arguments)
Method chaining is so beautiful, isn’t it?
That’s a Lot of Dots
What I like about this entire idea is that it turned the “dot operator” into something even more useful. It provides consistency in saying “Hey, I’m the really important object here.” And it allowed us to take what can sometimes be a confusing implicit this
and make it explicit but tiny. It also forms a sort of symmetry between method definitions and their use, since the parameter/argument with the dot is actually in the parameter list, with a dot.
Lambdas
One of the really cool things about Kotlin is what you can do with their lambdas. It has all sorts of shortcuts and special features, such as:
- If a lambda takes 0 or 1 arguments, leave out the parameter list. Also, if it takes 1, and you leave out the parameter list, you can use
it
as the implicit parameter name. - Lambdas with Receivers, or as Iike to call them, Extension Lambdas. This is where one of the “parameters” is a receiver which can be used with an implicit or explicit
this
in the lambda body. This is especially useful in internal DSLs. - Since:
- a lambda is surrounded by curly brackets (
{}
) - the parameter list can be left out most of the time
- if a lambda is the last argument of the function it’s being passed into, it can moved outside of the parentheses (and if it’s the only argument, there need not be any parentheses)
- a lambda is surrounded by curly brackets (
Then using these kinds of lambdas can create functions that look like they’re language features.
There’s just one problem: When you define the function that takes a lambda, you have to specify at that point whether the lambda has a receiver or not. This leads to a few functions that seem to double or triple up a bit in their standard library (also()
and apply()
; let()
, run()
, and with()
). They do pretty much the same thing with the only difference being that the lambda parameter in one doesn’t have a receiver while the lambda parameter in the other does.
We could remove that problem with our function system. Instead of defining let()
, run()
, and with()
like so:
fun <T, R> T.let(block: (T) -> R): R { … } fun <T, R> T.run(block: T.() -> R): R { … } fun <T, R> with(receiver: T, block: T.() -> R): R { … }
You could make just the one with()
function like so:
fun <T, R> with(receiver.: T, block: (T.) -> R) : R { … }
Now, with receiver
being a subject parameter, it can be called the 3 different ways (OO, procedural, or functional). But each of those three different ways can also decide whether to write the lambda to “use a receiver” or not. When you don’t specify the lambda’s parameters, it doesn’t make a difference, since the single parameter is implied to be a dot, which replaces it
and this
(whether implicitly or explicitly used).
By the way, if you didn’t notice, this also takes away the need for explicit extension methods, since any function that marks a subject can be used like it’s a method on the class. We’ve eliminated the need for half of Kotlin’s awesome features with inclusion of our one (and a half?) feature(s).
Functions Without Parentheses and Commas
Disclaimer: in some cases, by going down this rabbit hole, you may actually need to use more parentheses than you did before (notably when you start going crazy with operators), but most of these parentheses should probably have been there all along. More on that later. Also, we will not be looking at function definitions – I have been struggling with a possible syntax that doesn’t have at least parens while still having all the features I want – only function calls.
Did you know that there are languages out there that don’t use parentheses or commas when calling functions? Lisps don’t use commas, and they move the parentheses to surrounding the whole function call instead of just the arguments. Remove those parentheses, and you practically have Haskell (that’s not true, but at least a basic function call would look the same).
In Haskell, calling a function is as simple as function arg1 arg2 arg3
. Why is this desirable? Because then you can remove so many of those pesky special characters that slow you down while typing. Also, it can make internal APIs look that much cleaner and read more like prose (always a good goal to strive for, if you can). For instance, Java’s Lock
s have a method called tryLock()
that takes a number and TimeUnit
. Normally, with a static import of TimeUnit
, a call to this would look something like this:
lock.tryLock(500, MILLISECONDS)
But if you were to apply a minor tweak and get rid of parens and commas, it could look like this:
lock.tryFor 500 MILLISECONDS
Or, if you applied our new abilities to the function plus allowed named arguments, it could look like this:
tryToLock lock. for=500 MILLISECONDS
Man, that reads like elegant prose. It would be nice to get rid of the =
and the dot, but I’m not sure of a good way to do that, or if it’s desirable enough in the long run. As for the name, tryToLock
, I originally used just tryTo
, which worked, but was a bad idea. It assumed that the lock being passed it would be called lock
or something close enough (a generally safe assumption, actually, in this case). The problem is that the name of the variable is meant to be a noun, but we were using it like a verb. It doesn’t happen very often that a noun will match its verb form perfectly, if it even has one. If the name had been something more like myReadLock
, that would have worked terribly.
Also, as an aside, note that the numeric argument is named for
. When looked at within the function definition, it’s not all that great. But, if you’re working in a language that allows named arguments, try to think about whether you could use little words like this as the parameter name to make calls with named arguments read nicely. It doesn’t have to be in a language without parens and commas. Even in Kotlin, where the call would look like
lock.tryToLock(for=500, MILLISECONDS)
It reads more prose-y. Even better, though, would be
lock.tryToLockFor(500, MILLISECONDS)
Which kind of goes against my argument…
But I’m getting off topic.
The point is that it saves keystrokes and can read more like prose.
Operators and Infix Functions
There’s an additional benefit to skipping out on parens and commas: there’s no need for special new syntax to define functions as operators and infix functions.
This requires me to tell you something that I probably should have told you earlier, but I was leaving it as a surprise for this section. You can put a space between the subject and the “method” call. So our first try at removing parens and commas earlier could look more like this:
lock. tryFor 500 MILLISECONDS
And why wouldn’t you be able to put a space there? lock.
is just another argument. It’s just in a special spot. There’s a space between the function name and the other arguments, so why shouldn’t there be one between the subject and the function name. In fact, when the subject is put after the function name, it does get a space. So why not in this case?
Do you see how this helps us get operators and infix functions without any more syntax? Let’s say the language didn’t have any built-in operators and also allowed most special characters in names. Then you could define the integer addition operator as a function like this:
fun +(operand1.: Int, operand2:Int): Int { … }
Which could then be used like this:
val x = 12. + 15
Granted, that dot is distracting. If you feel that would get annoying, you could have an operator
keyword to apply to a function to denote that the subject parameter will always come first and does not need a dot. But even still, this covers almost every case that Kotlin’s operator
keyword does, as well as Kotlin’s infix
keyword. If you don’t mind the dot, and would like the operator function to be called in the other two ways as well, you don’t even need that keyword.
When in Doubt, Paren it Out
Languages that use functions instead of proper operators have an interesting property to them. They have almost no operator precedence to worry about. Things like the assignment operator is still sometimes baked in (not in Lisps…), and will need to have a lower precedence than functions, but that would cover 90% of baked-in precedence levels. What this does is make something like this:
a + b < c * d OR a / b > c - d
Very difficult to interpret. The mathematical operators, comparison operators, and logical operators are all just functions, meaning they all have the same precedence. As it is, going left-to-right would run us into trouble at the multiplication operator. Up until then, we only had a + b < c
, which comes out to a boolean value, with is then multiplied with c
. Multiplying a number with a boolean is rarely a possibility in languages. So, we need some way to assign precedence. That way is to use parentheses. In a normal language you may have used some parens for a little bit of clarity, like this:
(a + b < c * d) OR (a / b > c - d)
But maybe not. You wouldn’t need to. The precedence table would do the multiplication and addition first, addition and subtraction next, then the comparison, and finally do the logical operator (though short-circuiting would make it so that the right side isn’t computed until (and unless) the OR determines the left side to be false.
Without any precedence, you would need to type
((a + b) < (c * d)) OR ((a / b) > (c - d))
Which is arguably better to do anyway. In fact, that’s exactly what you have to do in lisps (though both operands are to the right of the operator) and Haskell (as far as I know. I only have passing glances at what Haskell looks like). Of course, letting special characters be used in function names which can be used as operators just opens up the big ol’ can of worms of allowing people to come up with their own completely ridiculous set of esoteric and confusing operators (probably one of my most hated things about Haskell), which is what made a lot of languages to swear off any kind of operator overloading.
I told you that some cases would require even more parens than normal. Personally, I think it’s worth the hassle to need them. Especially since I hate the fact that AND and OR have different precedences in so many languages. Just because one of them is “addition” in boolean algebra and the other is “multiplication”, that doesn’t mean that that’s the way it should be ordered in precedences. Grrah!
Anyway… I think that’s pretty much everything I wanted to share with you about all of this. Except…
Side Note: Names
When a language allows for more special characters to be used in names, it opens up my favorite naming convention. Normally, in highly restricted languages, you can pretty much only use underscores or capitalization to separate words. Each has the distinct disadvantage of requiring the Shift key, which I have a high enough tendency to screw up somehow that it really frustrates me.
After learning a bit about Clojure and seeing them use hyphens to break up names (when translated to Java, it gets transformed to using camel case), I fell in love. It places space between the words, making it easier to read, like an underscore does, but it doesn’t require the Shift key like underscore does. So, to you language designers out there: if you can find a way to allow hyphens in names, it would be appreciated if you did it. Also question marks. The way that Clojure code postfixes functions that return booleans with a question mark is really helpful and satisfying.
Okay, now I’m done. Onwards, to the outro!
Outro
What did you think? Does it all sound a bit silly? Do you think this is actually brilliant and wish that a language existed with these ideas? Let me know in the comments below.
Just so you know, I plan to someday maybe use JetBrains’ MPS to make a language with this idea and have it translate to Python ASTs and Kotlin/Java.
Also, next week, if I have the time, I’m going to show you some Python decorators that will allow you to use functions in a similar way as this. It’s just an idea right now, and I think it will work. We shall see.
Bye!
Published on Java Code Geeks with permission by Jacob Zimmerman, partner at our JCG program. See the original article here: A New Idea For Functions Opinions expressed by Java Code Geeks contributors are their own. |
You lost me at ” I would prefer to use this system without the need for parentheses or commas,”
That’s fine. I figured many people wouldn’t actually favor that idea.