Skip to content

Instantly share code, notes, and snippets.

@Dobiasd
Created August 27, 2024 15:12
Show Gist options
  • Save Dobiasd/bb9d38a027cf3164e66996dd9e955481 to your computer and use it in GitHub Desktop.
Save Dobiasd/bb9d38a027cf3164e66996dd9e955481 to your computer and use it in GitHub Desktop.

Idea: "ubiquefix" notation

This document outlines a new function-call syntax, which perhaps could be used in a (not-yet-existing) programming language.

Common notation syntaxes for (mathematical and other) expressions

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 +

Ubiquefix notation

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.

Partial function application

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.

But why?

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

Alleged ambiguities (and their resolution)

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

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

Is ubiquefix notation good?

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!

@origineering
Copy link

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.

@Dobiasd
Copy link
Author

Dobiasd commented Sep 2, 2024

@origineering Thanks a lot for the great comment! ❤️

Throughout the discussion on Reddit (and your comment here confirmed this), I understood why explicit piping operators are better. Exploring this weird ubiquefix part of the solution space was a fun and educational activity, which I'll let rest now. 😌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment