Scala 3 type lambdas. Example for "curried type parameters" - scala

Scala 3 has a powerful mechanism of expressing type constructors via type lambdas.
Even simple type lambdas can do powerful things like expressing partial application of a type constructor (see for ex https://stackoverflow.com/a/75428709/336184 ).
Docs mention "Curried Type Parameters" like
type TL = [X] =>> [Y] =>> (X, Y)
this looks like even more abstract thing.
Question:
Can anyone give a working example with an implementation of such a type lambda? Also - what is a practical purpose of such an abstraction? Any parallels in Haskell?

type TL = [X] =>> [Y] =>> ...
is the same as
type TL[X] = [Y] =>> ...
and should be the same as
type TL[X][Y] = ...
if there were multiple type-parameter lists (MTPL).
So [X] =>> [Y] =>> ... should be a way to introduce such type anonymously.
MTPL along with named type parameters could be useful for specifying some type parameters and inferring the rest of them.
Cannot prove equivalence with a path dependent type
Curried type "does not take type parameters"
https://contributors.scala-lang.org/t/multiple-type-parameter-lists-in-dotty-si-4719/2399
https://github.com/scala/bug/issues/4719
https://docs.scala-lang.org/scala3/reference/experimental/named-typeargs.html
For example specifying some type parameters and inferring the rest can be necessary for type-level calculations. Currently for type-level calculations people either make type parameters nested or use type members instead of type parameters.
When are dependent types needed in Shapeless?
In Haskell you can write
foo :: forall (f :: * -> * -> *) . ()
foo = ()
but in Scala without MTPL implemented, currently you can't write
def foo[F[_][_]]: Unit = ()
you can only write
def foo[F[_,_]]: Unit = ()
If there were MTPL then for a definition like def foo[F[_][_]]... it would be convenient having curried type lambdas [X] =>> [Y] =>> ..., you could use them at a call site foo[[X] =>> [Y] =>> ...].
In Haskell all type constructors are curried and there are no type lambdas
Lambda for type expressions in Haskell?

Related

What is "prolog style" in Scala?

The Scala 3 reference at https://docs.scala-lang.org/scala3/reference/metaprogramming/compiletime-ops.html mentions some "Prolog-like programming style" possible with Scala 3 mataprogramming:
The problem so far was that the Prolog-like programming style of
implicit search becomes viral: Once some construct depends on implicit
search it has to be written as a logic program itself.
But they all keep the viral nature of implicit search programs based
on logic programming.
I did some search but understood only that it somehow abuses the Scala compile-time behavior, and that something in it resembles Prolog.
What is that "Prolog-like programming style" and how it works? What namely resembles Prolog? Does it work in Scala 3?
Here is a basic correspondence to get you started:
Prolog-Atoms correspond to ordinary types.
Prolog-Variables correspond to type parameters.
What's called "Functors" here corresponds to type constructors.
Stating facts corresponds to providing constant givens.
Making queries corresponds to summoning corresponding proof-terms.
Let's start with a very simple example where we have just one atom, one prolog-"functor", one fact, and one query: this example from Wikipedia
cat(tom).
?- cat(tom).
>> Yes
can be directly translated into
trait Tom // Atom 'Tom'
trait Cat[X] // Functor 'Cat' with arity = 1
// Fact: cat(tom).
given Cat[Tom] with {}
// Query: ?- cat(tom). (is Tom a Cat?)
val query = summon[Cat[Tom]] // type-checks = Yes
Prolog rules correspond to parameterized given-definitions, the antecedents on the right side correspond to the using-parameters in the parameter list. For example, the classical syllogism example expressed in Prolog as
man(socrates).
mortal(X) :- man(X).
?- mortal(socrates).
>> True
can be encoded with a parameterized given that uses Man[X] and produces a proof of Mortal[X]:
trait Socrates
trait Man[X]
trait Mortal[X]
given socratesIsAMan: Man[Socrates] with {}
given allMenAreMortal[X](using m: Man[X]): Mortal[X] with {}
val query = summon[Mortal[Socrates]]
You can use scala3-compiler -Xprint:typer to see the composite proof term that the compiler generated to prove that Socrates is Mortal:
allMenAreMortal[Socrates](socratesIsAMan)
Knowing how to encode rules allows you to encode the more complicated example from the Wikipedia:
mother_child(trude, sally).
father_child(tom, sally).
father_child(tom, erica).
father_child(mike, tom).
sibling(X, Y) :- parent_child(Z, X), parent_child(Z, Y).
parent_child(X, Y) :- father_child(X, Y).
parent_child(X, Y) :- mother_child(X, Y).
?- sibling(sally, erica).
>> Yes
as follows:
trait Trude
trait Sally
trait Tom
trait Erica
trait Mike
trait FatherChild[F, C]
trait MotherChild[M, C]
trait ParentChild[P, C]
trait Sibling[X, Y]
given MotherChild[Trude, Sally] with {}
given FatherChild[Tom, Sally] with {}
given FatherChild[Tom, Erica] with {}
given FatherChild[Mike, Tom] with {}
given [X, Y, Z](using pczx: ParentChild[Z, X], pczy: ParentChild[Z, Y])
: Sibling[X, Y] with {}
given fatherhoodImpliesParentship[X, Y](using fc: FatherChild[X, Y])
: ParentChild[X, Y] with {}
given motherhoodImpliesParentship[X, Y](using mc: MotherChild[X, Y])
: ParentChild[X, Y] with {}
val query = summon[Sibling[Erica, Sally]] // Yes
Here, the compiler will generate a proof term that explains that Erica and Sally are Siblings because they have the same father Tom:
given_Sibling_X_Y[Erica, Sally, Tom](
fatherhoodImpliesParentship[Tom, Erica](given_FatherChild_Tom_Erica),
fatherhoodImpliesParentship[Tom, Sally](given_FatherChild_Tom_Sally)
)
More generally, conjunctions are encoded by multiple using-parameters, and disjunctions are encoded by multiple givens with the same result type:
// We can write "X /\ Y" as infix operator for conjunction
case class /\[A, B](a: A, b: B)
// We can write "X \/ Y" as infix operator for disjunctions
enum \/[+A, +B]:
case Inl(a: A)
case Inr(b: B)
// Inference for conjunctions: multiple parameters in `using`
given [A, B](using a:A, b: B): (A /\ B) = /\(a, b)
// Inference for disjunctions: multiple rules
given [A](using a: A): \/[A, Nothing] = \/.Inl(a)
given [B](using b: B): \/[Nothing, B] = \/.Inr(b)
// Example:
trait X
trait Y
trait Z
trait W
given X with { override def toString = "X" }
given W with { override def toString = "W" }
#main def query(): Unit =
println(summon[(X \/ Y) /\ (Z \/ W)])
// Finds a proof and prints `/\(Inl(X), Inr(W))`.
Since Scala 3, there is even negation available through util.NotGiven:
import scala.util.NotGiven
trait X
trait Y
trait /\[X, Y]
given X with {}
given [A, B](using a: A, b: B): /\[A, B] with {}
// Fails if we add `given Y with {}`
val query = summon[X /\ NotGiven[Y]]
Scala 3 adds a whole bunch of stuff on top of that, such as tuples (which are basically type-level lists) or computing with numeric / boolean / string singleton types, but I don't want to go too deeply into the details here.
Instead, I'd like to conclude by briefly sketching how it all fits into the landscape. The interesting difference between Prolog and Scala's type system is that the Scala compiler actually generates proof terms, and unlike in Prolog (where you get a simple "Yes"/"No"), those proof terms can carry around arbitrarily complicated computational content.
You might have noticed that in the examples above, the with {} mostly remained empty. This is usually not the case in the real code, quite the contrary: in the real code, you usually have some non-trivial definitions in the body of every given ... with { ... }. The reason why one is writing all those facts and inference rules is not for solving logical puzzles and obtaining "Yes" / "No" answers, but for generating huge complicated proof terms that do useful stuff.
The way it works is usually as follows:
Suppose you want to obtain a thing X
You import some big-&-smart library that knows how to construct a variety of things similar to the desired X
You use the "predicates" ( = typeclasses) / facts / rules ( = givens) of that library to express very precisely the properties that you want the thing X to have
If your description is precise enough, the library & the Scala compiler is able to summon the thing X purely from its type description.
So, in your average programming language, you have to write out all the terms manually. In Scala 3, you can instead specify the desired properties of the desired term through types, and the compiler will use this Prolog-like term inference system to summon a term with the right properties (given the right libraries, that is).

What is the type alias rules in Scala?

So, just can't understand why does this compiles?
type <=[B, A] = A => B
type F[A] = Double <= A //why our alias <= is allowed here?
What is the syntax rule of forming type aliases which allows constructions like these?
Can we free play only with the order => here like in this case?
There is a very simple rule that every type A B C is the same as B[A, C]. Evidently this only works for types with 2 type parameters.
I could not find the definition of =>
Yeah, it is embedded in the compiler implementation / language specification.
But, as you just show, it is quite easy to reimplement in on userland; it would look like this:
type =>[+A, +B] = Function1[A, B]
how Scala compiler parse an alias expression like sequence of types Type1 Type2 Type3
The language specs say that it supports infix types, so things like: A OP B are equivalent to OP[A, B]

Universally Quantified Types in Haskell and Scala?

Existentially quantified types explains:
any use of a lowercase type implicitly begins with a forall keyword, so the two type declarations for map are equivalent, as are the declarations below:
id :: a -> a
id :: forall a . a -> a
Given Scala's scala.Predef#identity, is there a forall equivalent, i.e. per Haskell's above second function?
There is no explicit forall in Scala, but it has equivalents to the different ways explicit foralls are used in Haskell:
To enable ScopedTypeVariables: not necessary, as Scala type variables are scoped by default.
For existential types: Scala supports them directly.
For higher-rank types, i.e. ones which have a forall nested under function arrows: consider this example:
f2 :: (forall a. a->a) -> Int -> Int
f2 f x = f x
In Scala [A](A => A) can't be used directly, but
trait Poly1 {
def apply[A](x: A): A
}
is equivalent to it, so you can implement f2:
def f2(p: Poly1, x: Int) = p(x)
I am not a Scala expert, but my understanding is that all Scala type parameters are universally quantified (i.e. declared with a forall) unless they are explicitly quantified with forSome. See, for instance:
What is the forSome keyword in Scala for?
In Haskell we use the forall keyword to declare existential types. It may seem counter-intuitive to use forall when we mean for some, but it can be done using the logical equivalence of the following statements:
(forSome x. P(x)) implies Q
(forall x. P(x) implies Q)
Here Q is a statement not containing x.

Scala Function.tupled and Function.untupled equivalent for variable arity, or, calling variable arity function with tuple

I was trying to do some stuff last night around accepting and calling a generic function (i.e. the type is known at the call site, but potentially varies across call sites, so the definition should be generic across arities).
For example, suppose I have a function f: (A, B, C, ...) => Z. (There are actually many such fs, which I do not know in advance, and so I cannot fix the types nor count of A, B, C, ..., Z.)
I'm trying to achieve the following.
How do I call f generically with an instance of (A, B, C, ...)? If the signature of f were known in advance, then I could do something involving Function.tupled f or equivalent.
How do I define another function or method (for example, some object's apply method) with the same signature as f? That is to say, how do I define a g for which g(a, b, c, ...) type checks if and only if f(a, b, c, ...) type checks? I was looking into Shapeless's HList for this. From what I can tell so far, HList at least solves the "representing an arbitrary arity args list" issue, and also, Shapeless would solve the conversion to and from tuple issue. However, I'm still not sure I understand how this would fit in with a function of generic arity, if at all.
How do I define another function or method with a related type signature to f? The biggest example that comes to mind now is some h: (A, B, C, ...) => SomeErrorThing[Z] \/ Z.
I remember watching a conference presentation on Shapeless some time ago. While the presenter did not explicitly demonstrate these things, what they did demonstrate (various techniques around abstracting/genericizing tuples vs HLists) would lead me to believe that similar things as the above are possible with the same tools.
Thanks in advance!
Yes, Shapeless can absolutely help you here. Suppose for example that we want to take a function of arbitrary arity and turn it into a function of the same arity but with the return type wrapped in Option (I think this will hit all three points of your question).
To keep things simple I'll just say the Option is always Some. This takes a pretty dense four lines:
import shapeless._, ops.function._
def wrap[F, I <: HList, O](f: F)(implicit
ftp: FnToProduct.Aux[F, I => O],
ffp: FnFromProduct[I => Option[O]]
): ffp.Out = ffp(i => Some(ftp(f)(i)))
We can show that it works:
scala> wrap((i: Int) => i + 1)
res0: Int => Option[Int] = <function1>
scala> wrap((i: Int, s: String, t: String) => (s * i) + t)
res1: (Int, String, String) => Option[String] = <function3>
scala> res1(3, "foo", "bar")
res2: Option[String] = Some(foofoofoobar)
Note the appropriate static return types. Now for how it works:
The FnToProduct type class provides evidence that some type F is a FunctionN (for some N) that can be converted into a function from some HList to the original output type. The HList function (a Function1, to be precise) is the Out type member of the instance, or the second type parameter of the FnToProduct.Aux helper.
FnFromProduct does the reverse—it's evidence that some F is a Function1 from an HList to some output type that can be converted into a function of some arity to that output type.
In our wrap method, we use FnToProduct.Aux to constrain the Out of the FnToProduct instance for F in such a way that we can refer to the HList parameter list and the O result type in the type of our FnFromProduct instance. The implementation is then pretty straightforward—we just apply the instances in the appropriate places.
This may all seem very complicated, but once you've worked with this kind of generic programming in Scala for a while it becomes more or less intuitive, and we'd of course be happy to answer more specific questions about your use case.

Weird behavior of & function in Set

Set is defined as Set[A]. It takes a in-variant parameter. Doing below works as expected as we are passing co-variant argument:
scala> val a = Set(new Object)
a: scala.collection.immutable.Set[Object] = Set(java.lang.Object#118c38f)
scala> val b = Set("hi")
b: scala.collection.immutable.Set[String] = Set(hi)
scala> a & b
<console>:10: error: type mismatch;
found : scala.collection.immutable.Set[String]
required: scala.collection.GenSet[Object]
Note: String <: Object, but trait GenSet is invariant in type A.
You may wish to investigate a wildcard type such as `_ <: Object`. (SLS 3.2.10)
a & b
But the below works:
scala> Set(new Object) & Set("hi")
res1: scala.collection.immutable.Set[Object] = Set()
Above as I see it, the scala compiler converts Set("hi") to Set[Object] type and hence works.
What is the type-inference doing here? Can someone please link to specification explaining the behavior and when does it happen in general? Shouldn't it be throwing a compile time error for such cases? As 2 different output for the same operation type.
Not sure, but I think what you're looking for is described in the language spec under "Local Type Inference" (at this time of writing, section 6.26.4 on page 100).
Local type inference infers type arguments to be passed to expressions of polymorphic type. Say e is of type [ a1 >: L1 <: U1, ..., an >: Ln <: Un ] T and no explicit type
parameters are given.
Local type inference converts this expression to a type application e [ T1, ..., Tn ]. The choice of the type arguments T1, ..., Tn depends on the context in which the expression appears and on the expected type pt. There are three cases.
[ ... ]
If the expression e appears as a value without being applied to value arguments, the type arguments are inferred by solving a constraint system which relates the expression's type T with the expected type pt. Without loss of generality we can assume that T is a value type; if it is a method type we apply eta-expansion to convert it to a function type. Solving means finding a substitution σ of types Ti for the type parameters ai such that
None of inferred types Ti is a singleton type
All type parameter bounds are respected, i.e. σ Li <: σ ai and σ ai <: σ Ui for i = 1, ..., n.
The expression's type conforms to the expected type, i.e. σ T <: σ pt.
It is a compile time error if no such substitution exists. If several substitutions exist, local-type inference will choose for each type variable ai a minimal or maximal type Ti of the solution space. A maximal type Ti will be chosen if the type parameter ai appears contravariantly in the type T of the expression. A minimal type Ti will be chosen in all other situations, i.e. if the variable appears covariantly, nonvariantly or not at all in the type T. We call such a substitution an optimal solution of the given constraint system for the type T.
In short: Scalac has to choose values for the generic types that you omitted, and it picks the most specific choices possible, under the constraint that the result compiles.
The expression Set("hi") can be either a scala.collection.immutable.Set[String] or a scala.collection.immutable.Set[Object], depending on what the context requires. (A String is a valid Object, of course.) When you write this:
Set(new Object) & Set("hi")
the context requires Set[Object], so that's the type that's inferred; but when you write this:
val b = Set("hi")
the context doesn't specify, so the more-specific type Set[String] is chosen, which (as you expected) then makes a & b be ill-typed.