I agree we don’t need both function form and method form, that’s too complicated. But the method form has the huge disadvantage that it again needs an underscore to indicate absence of the name. Since the unnamed form should be the norm, that’s a no-go for me.
I think the function form is a better way to look at the issue. But we don’t need to make our lives life too complicated by repeating phantom parameters on the right of the =
. Just interpret the =>
as contextual ?=>
since all givens are applied implicitly anyway. Then you are basically back at the syntax proposed in SIP 64.
But I think we could make the analogy with functions even stronger than before. I am adding the following section to the SIP which describes a small variation to the function syntax.
Alternative: Reinforce Similarity with Function Types
A reservation against the new syntax that is sometimes brought up is that the =>
feels strange. I personally find the =>
quite natural since it means implication, which is exactly what we want to express when we write a conditional given. This also corresponds to the meaning of arrow in functions since by the Curry-Howard isomorphism function types correspond to implications in logic. Besides =>
is also used in other languages that support type classes (e.g.: Haskell).
As an example, the most natural reading of
given [A: Ord] => Ord[List[A]]
is if A
is Ord
then List[A]
is Ord
, or, equivalently, A
is Ord
implies List[A]
is Ord
, hence the =>
. Another way to see this is that the given clause establishes a context function of type [A: Ord] ?=> Ord[List]A]]
that is automatically applied to evidence arguments of type Ord[A]
and that yields instances of type Ord[List[A]]
. Since givens are in any case applied automatically to all their arguments, we don’t need to specify that separately with ?=>
, a simple =>
arrow is sufficiently clear and is easier to read.
Once one has internalized the analogy with implications and functions, one
could argue the opposite, namely that the =>
in a given clause is not sufficiently function-like. For instance, given [A] => F[A]
looks like it implements a function type, but given[A](using B[A]) => F[A]
looks like a mixture between a function type and a method signature.
A more radical and in some sense cleaner alternative is to decree that a given should always look like it implements a type. Conditional givens should look like they implement function types. Examples:
// Typeclass with context bound, as before
given [A: Ord] => Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
// Typeclass with context parameter, instead of using clause
given [A] => Ord[A] => Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
// Alias with context bound, as before
given [A: Ord] => Ord[List[A]] =
ListOrd[A]
// Alias with with context parameter, instead of using clause
given [A] => Ord[A] => Ord[List[A]] =
ListOrd[A]()
For completeness I also show two cases where the given clause uses names for
both arguments and the clause as a whole (in the prefix style)
// Named typeclass with named context parameter
given listOrd: [A] => (ord: Ord[A]) => Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
// Named alias with named context parameter
given listOrd: [A] => (ord: Ord[A]) => Ord[List[A]] =
ListOrd[A]()
The new syntax fits exactly the approach of seeing conditional givens as implications: For instance,
[A] => Ord[A] => Ord[List[A]]
can be read as:
If A is a type, then if
A
isOrd
, thenList[A]
isOrd
.
I think this is overall the cleanest proposal. For completeness here is the delta
in the syntax description:
GivenDef ::= [id ':'] GivenSig
GivenSig ::= GivenType ([‘=’ Expr] | TemplateBody)
| ConstrApps TemplateBody
| GivenConditional '=>' GivenSig
GivenConditional ::= DefTypeParamClause | DefTermParamClause | '(' FunArgTypes ')'
GivenType ::= AnnotType {id [nl] AnnotType}
This would also give a more regular and familiar syntax to by-name givens:
var ctx = ...
given () => Context = ctx
Indeed, since we know =>
means ?=>
in givens, this defines a value of type () ?=> Context
, which is exactly the same as a by-name parameter type.
Possible ambiguities
-
If one wants to define a given for an a actual function type (which is probably not advisable in practice), one needs to enclose the function type in parentheses, i.e.
given ([A] => F[A])
. This is true in the currently implemented syntax and stays true for all discussed change proposals. -
The double meaning of
:
with optional prefix names is resolved as usual. A:
at the end of a line starts a nested definition block. If for some obscure reason one wants to define a named given on multiple lines, one has to format it as follows:given intOrd : Ord = ... given intOrd : Ord: def concat(x: Int, y: Int) = ...
Finally, for systematic comparison, here is the listing of all 9x2 cases discussed previously with the proposed alternative syntax. Only the 3rd, 6th, and 9th case are different from what was shown before.
Unnamed:
// Simple typeclass
given Ord[Int]:
def compare(x: Int, y: Int) = ...
// Parameterized typeclass with context bound
given [A: Ord] => Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
// Parameterized typeclass with context parameter
given [A] => Ord[A] => Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
// Simple alias
given Ord[Int] = IntOrd()
// Parameterized alias with context bound
given [A: Ord] => Ord[List[A]] =
ListOrd[A]()
// Parameterized alias with context parameter
given [A] => Ord[A] => Ord[List[A]] =
ListOrd[A]()
// Concrete class instance
given Context()
// Abstract or deferred given
given Context = deferred
// By-name given
given () => Context = curCtx
Named:
// Simple typeclass
given intOrd: Ord[Int]:
def compare(x: Int, y: Int) = ...
// Parameterized typeclass with context bound
given listOrd: [A: Ord] => Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
// Parameterized typeclass with context parameter
given listOrd: [A] => Ord[A] => Ord[List[A]]:
def compare(x: List[A], y: List[A]) = ...
// Simple alias
given intOrd: Ord[Int] = IntOrd()
// Parameterized alias with context bound
given listOrd: [A: Ord] => Ord[List[A]] =
ListOrd[A]()
// Parameterized alias with context parameter
given listOrd: [A] => Ord[A] => Ord[List[A]] =
ListOrd[A]()
// Concrete class instance
given context: Context()
// Abstract or deferred given
given context: Context = deferred
// By-name given
given context: () => Context = curCtx