Enum candy: the last mile

When access to term members inside an ADT is exclusively done via pattern matching or combinators, having to name members is tedious and useless. For instance:

enum Tree[+T]:
  case B(left: Tree[T], value: T, right: Tree[T])
  case E

  def fold[A](onEmpty: A, onBranch: (A, T, A) => A): A =
    def go(x: Tree[T]): TailRec[A] = x match
      case B(l, v, r) => tailcall:
        for l <- go(l)
            r <- go(r)
        yield onBranch(l, v, r)
      case E => done(onEmpty)
    
    go(this).result

The names left, value and right in Tree.B are never used. Would it be possible to synthesise names at compile-time and let developers write:

enum Tree[+T]:
  case B(Tree[T], T, Tree[T])
  case E

? We found that our datatypes fall into two categories: domain objects where names are relevant, and eDSL which are often (co-)recursive ADTs and come with interpreters/compilers, where names are irrelevant.

I guess inferring _1, _2 etc would be consistent - accessors with these names are already generated regardless of whichever parameter names you define

3 Likes

I do not think it is worth making the syntax less regular. You can always name parameters a, b, c or p1, p2, p3 if you are not interested in those names.

That said, it might be possible to have unused parameters unnamed anywhere by using underscore, like:

case B(_: Tree[T], _: T, _: Tree[T])

The same syntax could be used in ordinary function signatures.

9 Likes

Only problem is the Scala community still suffers from post underscore disorder :joy: and Scala 3 did a reasonably good job at addressing it.

Granted, but this is a use case where underscore seems consistent with the its primary usage. (As I usually refer to it, underscore is the “whatever” operator, when you really don’t want a name.) Using it like this, to indicate that we are only going to care about these members positionally, makes some intuitive sense.

2 Likes

What is the benefit of that over

case B(l: Tree[T], t: T, r: Tree[T])

Why is that?

It tries to satisfy the need in the OP specified as “having to name members is tedious and useless”.

Intent is clear, same as with function literals or pattern matchings. One could ask - what is the point of being able to write (x, _) => x when I could write (x, a) => x?

1 Like

See Old style tuple accessors on normal case classes in Scala 3 - Question - Scala Users

@OndrejSpanel I found myself agreeing with you on this one, but then it hit me: enum is a completely new construct with a few irregularities (case vs case class, no need for extends by default, etc.). Maybe this isn’t going too far away?

There’s other corners of the language that are currently pushing the boundaries of what is acceptable syntax (lately context bounds).

It makes the syntax more regular.

An implicit parameter is written

def f(using T)

and an explicit (albeit unnamed) parameter is written

def f(T)

which is nicer than

def f(@deprecatedName t: T) // disallow f(t = myT)

Instead of the weirdness that there is only one place where it matters if an identifier starts with a lowercase letter, namely in patterns, now there will be two, if a variable identifier must begin a var name, but otherwise it’s a type name.

The names are available internally as the positional names _1 etc, thereby resolving the additional weirdness that only “varargs” look different depending on whether you’re inside the function. As the saying goes, misery loves company.

Currently, when I have a signature such as your example,

def f(t: T, otherT: T)

I must grab my trusty old “Tables of Names for Software Engineers” off the shelf and cross-index my type names with well-known algorithms in order to make sure all the parameters have reasonable names.

Using positional names will not only save me hours of manual labor (manual in the sense that I must consult the ToN by hand), but it will spare me the agony before every code review when I have introduced a named element.

I have proposed that all code reviews be double-blind (so-called), where no one knows which variables are assigned to which computations until all the tests pass.

1 Like

I always wanted something like that. But I don’t know how it could work.

In strictly typed APIs you have often quite precise types (e.g. new-type wrappers ); but you actually also want good descriptive parameter names as otherwise the code is cryptic.

Something like:

def register(name: Name, dateOfBirth: DateOfBirth, birthplace: Birthplace)

Very often in such cases the type names and the parameter names match, except for the case.

It would be nice to be able to write something like:

def register(Name, DateOfBirth, Birthplace)

But how would one than access the parameters?

By position? No, for sure not, that would lead to even more cryptic code than using n, db, bp, and such.

For context bounds there is this proposal to make it possible to access the given parameters by the name of the type in the context bound (though some compiler internal “virtual object”, or something like that).

But I’m not sure this could work also for method / function parameters. My gut feeling is that it would open some cans of worms from the technical side. (So I’m not proposing anything here, just thinking out loud).

But it would be somehow attractive. At least if someone had an idea how this could work.

One could also think in the opposite direction and speculate what would happen if the compiler would expand

def register(name, dateOfBirth, birthplace)

to the original valid definition? This could just assume types of the params to be the uppercased names.

But all that looks kind of strange, TBH. Kind of random thoughts. Is this redundancy really a problem? IDK

Lambda can be used if parameter type can be omitted, ex:

val register = (name, dateOfBirth, birthplace) => { ... }

So we may not want to introduce another syntax, just relax the restriction to define a lambda.

How would you type such a lambda?

1 Like

Brief survey of languages that do support the creation of algebraic data types to some extent:

  • naming not possible or optional: haskell, f#, ocaml, elm, purescript, reasonml, rescript, clean, roc, unison, grain, gleam, flix, … rust

  • naming mandatory: typescript, kotlin, java, python, scala

my very biased opinion is that scala would feel more at home in the top list