-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type Classes #256
base: hkmc2
Are you sure you want to change the base?
Type Classes #256
Conversation
14e02f5
to
9b9f4be
Compare
Wow it works like magic 🫨🤓 // Monoid Example
abstract class Monoid[T] extends Semigroup[T] with
fun combine(a: T, b: T): T
fun empty: T
object IntAddMonoid extends Monoid[Int] with
fun combine(a: Int, b: Int): Int = a + b
fun empty: Int = 0
object IntMulMonoid extends Monoid[Int] with
fun combine(a: Int, b: Int): Int = a * b
fun empty: Int = 1
module M with
fun foldInt(x1: Int, x2: Int, x3: Int)(using m: Monoid[Int]): Int =
m.combine(x1, m.combine(x2, m.combine(x3, m.empty)))
fun fold[T](x1: T, x2: T, x3: T)(using m: Monoid[T]): T =
m.combine(x1, m.combine(x2, m.combine(x3, m.empty)))
use Monoid[Int] = IntAddMonoid
M.foldInt(2, 3, 4)
//│ = 9
use Monoid[Int] = IntMulMonoid
M.foldInt(2, 3, 4)
//│ = 24
use Monoid[Int] = IntAddMonoid
M.fold(1, 2, 3)
//│ FAILURE: Unexpected type error
//│ ═══[ERROR] Missing instance for context argument Param(‹›,m,Some(TyApp(SynthSel(Ref(globalThis:block#18),Ident(Monoid)),List(Ref(T))))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, nice draft changes!
TODO: Instance identifiers should not be able to be referred directly in the code.
Edit: Wait. Do we really want to do that? I thought in Scala implicit instances are in another name scope, but seems that's not the case 🤓
Indeed, there need be no such restriction.
- Instance definitions: add them into the context with their type signature as the key.
The key should be the type constructor of the type class being instantiated. It can be concrete or abstract (a type parameter or type member). As in fun foo[A](someA) = { use A = someA; ... }
BTW eventually we'll like to define things like:
module Foo[A](using Bar[A]) with
...
[baz, Foo.baz] // equivalent to `[this.baz, Foo(using Bar[A].baz]`
use Bar[Int] // possible to change the context here
Foo.baz // equivalent to `Foo(using Bar[Int]).baz`
...
But for this we first need to add support for parameterized modules.
@@ -178,6 +178,9 @@ extends Importer: | |||
|
|||
def term(tree: Tree, inAppPrefix: Bool = false): Ctxl[Term] = | |||
trace[Term](s"Elab term ${tree.showDbg}", r => s"~> $r"): | |||
def maybeModuleMethod(t: Term): Ctxl[Term] = | |||
if !inAppPrefix && ModuleChecker.isModuleMethodApp(t) then moduleMethod(t) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why !inAppPrefix
?
Admittedly we should document what the purpose of inAppPrefix
does: it's used to avoid generating undefined
-checks when a field being accessed is actually called like a method, because in this case undefined
will crash as needed already.
I don't think this has anything to do with module methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As its name suggests and by looking at the implementation of term
, I think this flag is true
only if the elaborator is evaluating the LHS of an App. Without this flag check, if the elaborator sees some App like f()()()
, it'll "further elaborate" the term 3 times, namely f()
, f()()
, f()()()
, while it is expected to be further elaborated only once with f()()()
!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sure, if we want to elaborate calls with multiple argument lists at once. Does this work well in your current implementation approach? What if only some of the argument lists are provided (ie it's partially applied)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't think of a way to have module methods partially applied. If we do that, the result may not be recognized as a module method and then cannot be further elaborated with type information. I remember we discussed this, and we ended up with to reject partial application on module methods. (not implemented yet)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the omitted parameter lists don't have any special feature in them (no using
, etc.) then there's no reason to forbid it, as it's functionally equivalent to a module method returning a lambda. There would be no reason to reject that.
I remember we discussed this, and we ended up with to reject partial application on module methods.
IIRC I was just suggesting to have this arbitrary implementation restriction to ease progress on the first PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright. Let's formally restrict partial application if the resulting function does require further elaboration with type information. Otherwise, let it behaves just like normal methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the resulting function does require further elaboration with type information
Note that we should also check that the result type also does not need module-method treatment. E.g., it shouldn't return a module!
val newCtx = ctx + (p.sym.name -> p.sym) | ||
val newFlags = if isCtx then flags.copy(ctx = true) else flags |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean that in (x: Int, using y: Str)
, both x
and y
are contextual? What's the point of allowing this confusing syntax?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this is what the code currently does. I'm going to fix it (and some other strange behaviors 🫨) later when I convert the draft PR into a PR.
@@ -924,6 +964,13 @@ extends Importer: | |||
case S(sym: BlockMemberSymbol) => sym.modTree.exists(isModule) | |||
case _ => false | |||
|
|||
def isModuleMethodApp(t: Term): Bool = t match |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better follow the "parse, don't validate" methodology (https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/).
Instead of returning a Boolean, just return an option of the relevant information so that info doesn't have to be retrieved separately in a way that might not be perfectly in sync, creating an unnecessary source of bugs.
log(s"Resolving instance definition $t") | ||
sign match | ||
case N => | ||
raise(ErrorReport(msg"Missing signature for instance ${t.toString}" -> N :: Nil)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally all the errors raised in your PR need to have a test reproducing them. Usually in a file with a name like BadTypeClasses.mls
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Theoretically the error is never raised because the elaboration promised that an instance must have a type signature. I guess later we can have some term other than TermDefinition
to describe an instance definition? I found it didn't fit our use case here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either it's "impossible" and you should make this report an internal error (e.g. by calling lastWords
, although we do need a better way of reporting internal errors, without necessarily terminating compilation) or it's possible but an error will have already been reported about this, in which case the correct behavior is to silently return some error term (with a code comment explaining why that's fine).
If only the type constructors are specified as keys, wouldn't the following application fail? use Monoid[Int] = IntAddMonoid
use Monoid[Str] = StrConcatMonoid
M.foldInt(2, 3, 4)
// resolves to M.foldInt(2, 3, 4)(using StrConcatMonoid) |
It does fail, indeed. I guess we could make the rules a wee bit more fine-grained: we could look for the innermost instance in scope that could match the type query, where only those types explicitly specified (like the In any case, the map should still use keys that are type constructor symbols and not signatures. You can't just use syntactic types as keys as several type expressions could be equivalent and represent the same semantic type. |
This (draft) PR introduces type classes by a minimal implementation of contextual parameters, including:
Parser rules for parsing
use
andusing
keywords.Elaborator rules for elaborating
use
andusing
keywords.A
use
definition is elaborated into aTermDefinition
of kind instance (Ins
). If there is no explicitly specified identifier with the instance, a synthesized identifier is used.If any parameter in the parameter list is modified by the
using
keyword, the whole parameter list becomes an "implicit parameter list".Further elaboration on module methods
Module method applications (or implicit applications) are further elaborated with the type information.
In particular, if there is any missing contextual argument lists, the elaborator will unify the argument lists with the parameter lists, with some placeholder
Elem
. TheseElem
s are populated later in the new implicit resolution pass.New pass: Implicit Resolution
A new pass called
ImplicitResolution
is introduced to resolve implicit arguments after elaboration.It traverses the elaboration tree, find:
TODO: Move rules of module methods to this pass.