DEV Community

KentaroMorishita
KentaroMorishita

Posted on

What If a Function Definition Looked Like Its Type?

A Seseragi function can be defined like this:

fn add a: Int -> b: Int -> Int =
  a + b
Enter fullscreen mode Exit fullscreen mode

At first, the obvious visual differences are the missing parameter-list parentheses and commas.

But after using this syntax, those stopped being the part I liked most.

Remove the parameter names:

Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

The function type is still sitting there almost untouched.

When I look at the function definition, I can see the function's type directly inside it.

That may be the most interesting part of Seseragi's fn syntax.

The parameter names sit on the inputs of the function type

Look at the definition again:

fn add a: Int -> b: Int -> Int =
  a + b
Enter fullscreen mode Exit fullscreen mode

The type by itself is:

Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

Put the parameter names back:

a: Int -> b: Int -> Int
Enter fullscreen mode Exit fullscreen mode

In my head, this is less like "write two parameters, then write the return type" and more like:

take the function type Int -> Int -> Int and give its two inputs the names a and b.

The function type comes first conceptually. The definition adds the names that the body will use for those inputs, without changing the overall shape very much.

There is less mental conversion between "syntax for defining a function" and "syntax for describing a function's type."

Underneath that shape is ordinary currying

This works because Seseragi functions are curried.

Function arrows associate to the right, so:

Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

means:

Int -> (Int -> Int)
Enter fullscreen mode Exit fullscreen mode

Give the function one Int and what remains is:

Int -> Int
Enter fullscreen mode Exit fullscreen mode

Give that function another Int, and the result is an Int.

Function application follows the same structure:

add 1 2
Enter fullscreen mode Exit fullscreen mode

is applied from the left:

(add 1) 2
Enter fullscreen mode Exit fullscreen mode

Each argument advances through one arrow in the function type.

Seseragi's Tour treats this "multiple arguments become a sequence of one-argument function values" behavior directly as currying.

So currying is not an optional trick added after the function syntax was designed.

The definition is already describing a curried function in a shape close to its type.

That is the more accurate model.

Once I see it that way, parameter-list parentheses stop feeling necessary

TypeScript naturally writes:

function add(a: number, b: number): number {
  return a + b
}
Enter fullscreen mode Exit fullscreen mode

Rust naturally writes:

fn add(a: i32, b: i32) -> i32 {
    a + b
}
Enter fullscreen mode Exit fullscreen mode

In both languages, (a, b) is a parameter list treated visually as one unit.

Those syntaxes make perfect sense for those languages.

But the thing I want to remain visible in Seseragi is this continuous function type:

Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

If I add parameter-list parentheses, the surface begins to suggest "one grouped input containing two values."

But the actual Seseragi type is:

Int -> (Int -> Int)
Enter fullscreen mode Exit fullscreen mode

So I don't really think of the parentheses as something I removed to make the syntax shorter.

I brought the shape of the function type into the definition, and then there was no obvious job left for parameter-list parentheses.

Reducing punctuation was not the starting goal.

Tuples make the distinction much clearer

Compare these two types:

Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

and:

(Int, Int) -> Int
Enter fullscreen mode Exit fullscreen mode

They are different.

The first is a curried function. It accepts one Int and returns another function of type Int -> Int.

The second accepts one Tuple value, (Int, Int), and returns an Int.

In Seseragi, () and , already carry real Tuple meaning.

If (a, b) were also used merely as punctuation around two independent parameters, those two models would start looking more alike than they really are.

The current fn syntax declares parameters by name and type; it does not directly place a Tuple pattern in the parameter position.

But the important point here is not which pattern feature exists today.

It's the model:

Int -> Int -> Int and (Int, Int) -> Int are different types, and I want their definitions to look different too.

It's not that Seseragi refuses parentheses.

It's that parentheses already mean something.

Requiring a return type follows the same idea

Ordinary Seseragi fn definitions require an explicit return type.

So this is not valid:

fn add a: Int -> b: Int =
  a + b
Enter fullscreen mode Exit fullscreen mode

A compiler could probably infer that the body returns Int.

But if the final -> Int disappears, the visible function type in the definition becomes incomplete.

With:

fn add a: Int -> b: Int -> Int =
  a + b
Enter fullscreen mode Exit fullscreen mode

removing the names still leaves:

Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

The language also has practical reasons for keeping ordinary function return types explicit: module APIs and recursive functions benefit from a stable declared contract.

But aesthetically, I like another consequence:

information can be inferable to the compiler and still be useful enough to keep in source for the human reader.

Removing parentheses does not remove information about the function type.

Removing the return type does.

Shorter source is not automatically better source.

A "two-argument function" still advances one input at a time

In conversation, I will happily call add a two-argument function.

That's convenient language and nobody gets hurt.

But the type still tells a more precise story:

Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

Apply one Int and you have:

Int -> Int
Enter fullscreen mode Exit fullscreen mode

So this is natural:

let increment = add 1
Enter fullscreen mode Exit fullscreen mode

There is no special partial-application construct here.

Application simply moved through the first arrow and stopped.

What I find interesting about partial application isn't only that Seseragi supports it.

It's that you can almost predict the behavior directly from the way the function definition is written.

A function with no visible parameters still returns to the same model

Seseragi can write a function that receives no meaningful information like this:

fn heading -> String =
  "Hello"
Enter fullscreen mode Exit fullscreen mode

There is no visible parameter.

Semantically, though, the function has the shape:

Unit -> String
Enter fullscreen mode Exit fullscreen mode

Conceptually, you can imagine starting from:

fn heading unit: Unit -> String =
Enter fullscreen mode Exit fullscreen mode

and omitting the name unit because naming a Unit input adds no useful information.

Calling it is:

heading ()
Enter fullscreen mode Exit fullscreen mode

There is no separate zero-argument call syntax such as heading().

Again, the model returns to an ordinary A -> B function type.

effect fn has a compact form for a different reason

Ordinary fn keeps its return type explicit, but effect fn has a compact form:

pub effect fn main =
  println "Hello"
Enter fullscreen mode Exit fullscreen mode

At first glance, that seems to contradict everything above.

But writing effect fn already tells the reader that this function does not return an ordinary value directly. Its larger shape is:

Effect<R, E, A>
Enter fullscreen mode Exit fullscreen mode

From there, R can be inferred from the effects used by the body, and A from the successful result.

E is inferred too, although application code often transforms failures into a domain error explicitly with things such as mapError, so failure design still tends to appear in source.

When an explicit contract matters, the longer form is available:

effect fn greet name: String -> Unit
with Console
fails ConsoleError =
  println $ `hello ${name}`
Enter fullscreen mode Exit fullscreen mode

So the compact effect fn form is not saying "type information doesn't matter here."

The large fact that this function returns Effect is already present in the effect fn definition itself.

The remaining question is how much of R / E / A needs to be written explicitly at that point.

Haskell puts two lines next to each other; Seseragi makes them overlap

Haskell can write:

add :: Int -> Int -> Int
add a b = a + b
Enter fullscreen mode Exit fullscreen mode

The function type and definition are separate, and both are very direct.

Seseragi writes:

fn add a: Int -> b: Int -> Int =
  a + b
Enter fullscreen mode Exit fullscreen mode

The result looks almost like those two Haskell lines have been overlaid.

Take only the types:

Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

Take only the names:

add a b
Enter fullscreen mode Exit fullscreen mode

I didn't begin with a plan to import Haskell's syntax.

But once you try to define curried functions while keeping the function type visually close to the definition, it's interesting that the designs end up near some of the same territory.

In the end, this is the line I like

fn add a: Int -> b: Int -> Int =
  a + b
Enter fullscreen mode Exit fullscreen mode

The important part isn't simply that there are fewer parentheses.

It isn't simply that partial application works.

Those things follow naturally from the same model.

The part I like most is that if I remove the parameter names, I get:

Int -> Int -> Int
Enter fullscreen mode Exit fullscreen mode

almost unchanged.

Start with a curried function type.

Give its inputs names:

a: Int -> b: Int -> Int
Enter fullscreen mode Exit fullscreen mode

Then add the function name and body:

fn add a: Int -> b: Int -> Int =
  a + b
Enter fullscreen mode Exit fullscreen mode

Look at the function definition, and the function type is already there.

That has become one of my favorite small design choices in Seseragi.

https://seseragi.vercel.app/

Top comments (0)