This document outlines a new function-call syntax, which perhaps could be used in a (not-yet-existing) programming language.
function arg1 arg2
This is not only used in languages like Haskell etc, but also the function call syntax in many mainstream programming languages is somewhat similar, at least regarding the order of the three tokens:
function(arg1, arg2)
Examples:
+ 1 2
add(1, 2)
arg1 function arg2
It's used for binary functions (functions with an arity of 2), and is very well known from mathematical notation. Example:
1 + 2
arg1 arg2 function
This is not so common anymore. But in the past, it was popular in calculators, and it is used in stack-oriented programming languages such as Forth, etc.
Example:
1 2 +
This syntax combines prefix, infix, and postfix into a more general notation.
A function fills its parameter cavities by attracting surrounding values from left to right.
This means, all the following are valid, and they all mean the same thing:
function arg1 arg2
arg1 function arg2
arg1 arg2 function
For functions with more than two parameters, it works the same way:
function arg1 arg2 arg3
arg1 function arg2 arg3
arg1 arg2 function arg3
arg1 arg2 arg3 function
To disambiguate expressions containing more than one function application, expressions in ubiquefix notation are left-associative. The advantages of this choice will become apparent later.
A function fuses with its neighbor values until it's either full (i.e., fully applied) or until there are no more values left to fuse with.
Given a function taking an A
and B
, and returning a C
f : A, B -> C
and a value
a : A
f a
and a f
both result in a partially applied function of type
B -> C
Arguments from the left side of a function are shifted into the parameter cavities from the left. Arguments from the right side of a function are shifted into the parameter cavities from the right.
Example:
f : A, B, C -> D
a f : B, C -> D
a b f : C -> D
f c : A, B -> D
f b c : A -> D
a f c : B -> D
Here we can see that infix notation emerges automatically from first fusing with arguments on the left side (postfix style) and then fusing with arguments on the right side (prefix style):
f : A, B -> C
a f b
is just f
being first (partially) applied to a
((a f) b
) resulting in an intermediate function of
type B -> C
, which is then applied to c
.
Because the versatility of ubiquefix notation allows for quite expressive/readable code. Example:
Given a string containing a comma-separated list of integer values (e.g., 42,1,23
)
input : String
which we want to parse and then calculate the sum of their squares with these helper functions
split : String, Character -> List[String]
map : List[A], (A -> B) -> List[B]
stringToInteger : String -> Integer
square : Integer -> Integer
sum : List[Integer] -> Integer
we can construct a pipeline-like expression
input split ',' map stringToInteger map square sum
which can be read nicely from left to right. (Remember, ubiquefix notation is left-associative.)
Refactored a bit, it looks like this:
input splitOnComma stringsToIntegers squareIntegers
splitOnComma = split ','
stringsToIntegers = map stringToInteger
squareIntegers = map square
The definitions of these helper functions use partial function application (with prefix-like notation), which gives these helpers the following types:
splitOnComma : String -> List[String]
stringsToIntegers : List[String] -> List[Integer]
squareIntegers : List[Integer] -> Integer
The pipeline itself (input splitOnComma stringsToIntegers squareIntegers
) uses the postfix-notation style to let the
data flow from left to right through the listed functions.
Using function composition, we could also do
input doTheThing
doTheThing = splitOnComma . stringsToIntegers . squareIntegers
If two functions are next to each other, the ambiguity of which is applied to which, is resolved by the type system, i.e., there are no situations in which both ways ("apply f to g" and "apply g to f") would work. Example:
f : A -> B
g : (A -> B) -> C
f g
f
can't be applied to g
, thus g
is applied to f
. (The same is true if we swap the order in the expression, i.e.,
have g f
.)
In case neither direction is valid, it's just a type error.
Function composition can be implemented as a function provided by the standard library:
fwdCompose:(a->c) f:(a->b) g:(b->c) = composition
composition x = x f g
(a
, b
, and c
are type variables here, i.e., fwdCompose
is a generic function.)
And then be used like this:
fAndThenG = f fwdCompose g
I don't yet know.
Sure, the fact, that the order of functions and their arguments is not the same in each (sub-) expression takes getting used to. But maybe it's worth it. I'd love to find out!
i would submit that as stated above, without further qualifications, this suggestion of: “ubiquefix notation” is not recommended “as is”, because it will lead to much confusion for readers of the source-code:
A) it doesn't scale since it makes it difficult/impossible to distinguish verbs (functions/operators) from nouns (variables).
B) it requires doing away with operator precedence rules like PEMDAS which is a step down-instead of a step-up IMO.
In some PLs, the piping operators, that is: ⊲ (known as: “of”), and: ⊳ (known as: “to”), can be judiciously used to achieve the intended purpose of this article by relaxing operator/function ubity (aka: “ubiety”). This feature is known as “ubivariance”.
This is achievable because the piping operators are bi-directional to either left or right.
For example imagine we have a PL where the span function (typically known as “length” in most PLs) gives the number of elements/items in a functor object (like an array, list, set, matrix, etc.).
so:
span( myList[··] )
would give the length of the entire range of a 1-D array named: “myList”.
But by using the piping operators, we can effectively make the function named: “span” become a postfix or prefix operator.
observe:
myList[··] ⊳ span
or:
span ⊲ myList[··]
Conversely, we can effectively make an infix operator become a function.
e.g.
instead of:
a + b
we can express our intent this way:
+ ⊲ a , b
or as:
a , b ⊳ +
which is pretty nifty.
Note that we can chain compositions as well:
func(a + b)
can be expressed as:
func ⊲ + ⊲ a , b
or as:
func( + ⊲ a , b )
etc.
The secret to this magic is that the piping operators over-ride the natural default ubity of any verb they act on.