Skip to content
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

Typings bikeshedding #999

Open
vendethiel opened this issue Jan 13, 2018 · 55 comments
Open

Typings bikeshedding #999

vendethiel opened this issue Jan 13, 2018 · 55 comments

Comments

@vendethiel
Copy link
Contributor

vendethiel commented Jan 13, 2018

Some time ago (#803) I had started working on a PR to add type hints to the language.

That's still something I miss to this day, so I want to bring it back up.
There were a lot of discussions, so I think we should try to discuss it beforehand.

Typing symbol

I suggested the symbol @: to add a type annotation. Someone suggested ^. @gkz suggested ::.

The reason I want(ed) not to use :: is because it's rewritten to ID:prototype when it's parsed directly, at which point we have no info on whether it's going to be part of a signature of just a Parens (we only tag () as PARAM( and PARAM) after seeing ->).
We'd need to go through the tokens to mark ID:prototype as ASCR (or whatever else). We'd also need to be careful not to tag in "defaults" – that is, (x = -> ::) ->, (x ? ::) ->, (x and ::) ->, etc...

where to allow typings

I'd suggest we only allow type annotations in:

  • function declarations (function and ->/<-/...)
  • var
  • let

generics

I have one suggestion: [T](xs @: Array[T]) ->. The reason I suggest [] rather than <> is mostly for parse-ability (and because I'm used to Scala...). Tagging []() -> is easier than the ambiguities with <>, we just have to look for a ] when re-tagging a ( as a PARAM(, and cycle back to [ from there (since an array is never callable anyway).
(since [T]() -> and xs[T]() -> make no sense anyway)

Parsing the @: Array[T] part is harder, but still simpler than @: Array<T>, where the > would be parsed as a BIOP because it's directly followed by a )...


@rhendric I'm interested in your PoV, because it's a sensible topic, considering all the ambiguities to keep in mind :-).

@vendethiel
Copy link
Contributor Author

vendethiel commented Jan 13, 2018

As is the tradition, I've first implemented this with LEXER HACKS : 509cf13

$ ./bin/lsc -m none -bce '(x @: Map[Int, Array[Int]], y @: Int) ->' | tail -1
(function(x : Map<Int,Array<Int>>, y : Int){});

@rhendric
Copy link
Collaborator

I'm really glad you're still looking into this. I'm also really nervous about adding yet more syntax hacks—I don't know whether LS has already crossed over the line where writing a reasonable parser for it someday is basically impossible, but I'm hoping that nothing we'd do as part of adding types would push it over that line. I want to try to avoid committing any new lexical sins, if possible—adding more back-scanning to the -> strikes me as something to avoid.

The most bikesheddy question: how to spell has-type? I guess @: is okay. I think I would actually prefer  :: , with required spaces on both sides and followed by a token that can start a chain (otherwise, it's still ID:prototype). This is not a new lexer sin; other tokens do similar things ( .  is function composition but . is member access, and ++ checks token types on either side).

I also think that we should reuse as much existing LS syntax as possible, where applicable. For that reason, I would propose Array T instead of Array[T] or Array<T>—parse parameterized types as function calls, with the same rules about optional parentheses and comma insertion. This also means there wouldn't be anything hard about parsing (xs :: Array T) -> or (xs :: Array(T)) ->—it should be handled by existing code.

Given the above three opinions, how to handle generics? If type constructors are applied like functions, I would argue that they should be declared like functions—something like (T) ::> (xs :: Array T) ->, where ::> is a new token for type-level functions. Then ::> and -> can share the PARAM(-ifying code and no new lexer sins are added here either. (If we go with @:, I guess I would advocate @:> here, but now we're venturing into real made-up nonsense territory IMO.)

Here are some other things to be worried about (where relevant, this is largely from the perspective of Flow; I'm not as familiar with TypeScript):

  • Not having a way to annotate function return types may be limiting, but inserting a return type annotation before the -> would complicate backtracking. How else could we do that if we decide later on we want to?
  • We could get away with writing { foo: ?SomeType } as foo: SomeType?, but how would we want to write { foo?: SomeType }?
  • * is a type, and also already a messily overloaded token in LS.
  • Not a worry, more of a don't-forget-to: because the bitwise logic operators in LS are verbose, let's make sure the boolean logic operators can be used instead to construct intersection and union types.

@vendethiel
Copy link
Contributor Author

vendethiel commented Jan 17, 2018

As of right now...

So, for my current version (still a lexer hack) the return type syntax is @: Int ->, with params: (x @: Int) @: Int ->.

In the spirit of hacks, to not change Lexer.parameters, if we parse an arrow and our last is an ASCR, we put the token on the side while parsing parameters (there's a good reason I don't want to push it!).

type param syntax

I'm against adding ::>. It looks bad, and there's no reason we need it. If anything, if we want to go with the way you said (x :: Array Int or x :: Array(Int)), I think we'd be better off with a 2nd param list, e.g. (T)(x :: Array T) ->. I still prefer [T](x :: Array[T]) ->, though...

typings symbol

I'm more and more against ::. For historical reasons, I think we unspace ::, so (x :: Int) -> is x::(Int(->)). EDIT: oh, no, (x :: Int) is indeed x::(Int), but we "correctly" parse (x :: Int) ->.

@vendethiel
Copy link
Contributor Author

vendethiel commented Jan 17, 2018

I'm also really nervous about adding yet more syntax hacks

I just wanted to do this as a proof-of-concept.
I mean. It's really hard, because we do so many things at "lexer time" – parameter rewriting, etc. So we need to end up in a "valid" parse states after the lexer ran. Thankfully in this case, it seems we don't end up too poorly (as opposed to what I previously said in my last comment):

$ lsc -le '(f :: Map Int (Array Int)) ->'
NEWLINE:\n PARAM(:( ID:f ID:prototype ID:Map ID:Int ( ID:Array ID:Int ) )PARAM:) -> NEWLINE:\n

So implementing this syntax is fine. However... For the return type, it's much more complex. even if we use @:. We insert PARAM(,)PARAM when we see a ->.
My current lexer hack "folds" all the tokens after @: into the @: (so (x @: Array[Map Int]) -> is parsed as PARAM( ID ASCR )PARAM ->).

If we want to do it the "right way" (?) (no folding into ASCR), that means we need to change Lexer.parameters to try and go back "N" tokens (an indefinite number of them) to try and find ) @: so that we know we're parsing a signature...

On the other hand, if we go for a syntax like @: Map[Int, Array[String]] ->, the parsing is much easier, we either have a single lookbehind (a simple id) or until we find a matching [ for that ] (Array[Int]). We also have to throw away some dots (since x[3] is ID DOT [ STRNUM ]). At least until unions etc enter the field...

I don't know whether LS has already crossed over the line where writing a reasonable parser for it someday is basically impossible, but I'm hoping that nothing we'd do as part of adding types would push it over that line. I want to try to avoid committing any new lexical sins, if possible—adding more back-scanning to the -> strikes me as something to avoid.

Well, I don't think it would be impossible. We'd just need a parser that'd be less confused as to what it's looking at.

This also means there wouldn't be anything hard about parsing (xs :: Array T) -> or (xs :: Array(T)) ->—it should be handled by existing code.

As demonstrated, it doesn't, because some rules don't apply between PARAM( and )PARAM.

Not having a way to annotate function return types may be limiting, but inserting a return type annotation before the -> would complicate backtracking. How else could we do that if we decide later on we want to?

See ^

Flow stuff

We could get away with writing { foo: ?SomeType } as foo: SomeType?, but how would we want to write { foo?: SomeType }?

I have no idea how these two are different. Optional field vs nullable type?

  • is a type, and also already a messily overloaded token in LS.

It's a type??

@ozra
Copy link

ozra commented Jan 18, 2018

I like the idea of spaced :: — in my pet language there are hoards of situation where punctuation differ in meaning, depending on how it's spaced. Surprisingly I haven't found it confusing at all. YMMV.
Spaced :: means "here comes a type". And that's that.

# Obvious. Typed variable
x :: Int = 0

# Type aliases!?
MyBarCombo = :: BoozeBar<Qwo, Bzt> | (Barable<Qwo> & Boozable<Bzt>)

# Of course, param types and return type
foo = (a :: String, b :: Any?, c :: MyBarCombo) :: SuperBool ->
    console.log "#{a}, #{b ? ""}, #{c}"
    super-true

# casting / coercion!?
console.log "A real number:", ((47 * 23) :: Real)

# Extending on the alias above, crazy:
MyClass = ::
    (foo) ->
        @foo = foo + "rules"
        @i-give-up = "now"

@determin1st
Copy link

didnt dive into all the comments, sorry, but why not add some kind of parse logic marker at the top of the file with code, like

"use strict, ts_compat"

so it will be converted to globally accessible flag and checked where the compile logic works? so the livescript could be extended to anything.. or maybe it's an utopia)

@vendethiel
Copy link
Contributor Author

vendethiel commented Jan 18, 2018

in my pet language there are hoards of situation where punctuation differ in meaning, depending on how it's spaced.

in LS, we have:

  • calls use space (f a b), except for literals using ACI.
  • existential operator: a? b vs a ? b.
  • lines: a b .c, etc.
  • dot: a.b vs a . b
  • regex (f /a/ vs f / a / => disallowed)
  • ++ (f a++ b => f(a++, b) vs f a ++ b => f(a.concat(b)))
  • keywords (require ! a vs require! a).
  • ADI/ACI (a [b] vs a[b])
  • biop

So that ship sailed a loooooooooooooong time ago. Doesn't mean we need anymore of them. But yes, we can have dually-spaced :: mean "type ascription".

answering @ozra

# Obvious. Typed variable
x :: Int = 0
# vendethiel: my goal is to only allow this in let, var, and const.

# Type aliases!?
MyBarCombo = :: BoozeBar<Qwo, Bzt> | (Barable<Qwo> & Boozable<Bzt>)
# vendethiel: probably, but with a keyword. Also, no <>.

# Of course, param types and return type
foo = (a :: String, b :: Any?, c :: MyBarCombo) :: SuperBool ->
    console.log "#{a}, #{b ? ""}, #{c}"
    super-true
# vendethiel: that works locally.

# casting / coercion!?
console.log "A real number:", ((47 * 23) :: Real)
# vendethiel: maybe? it currently parses but doesn't do anything.

# Extending on the alias above, crazy:
MyClass = ::
    (foo) ->
        @foo = foo + "rules"
        @i-give-up = "now"
# vendethiel: what's that supposed to be?

@wryk
Copy link

wryk commented Jan 18, 2018

Type constructor

I prefer application syntax Map Int, Array(Int) over Map[Int, Array[Int]] but livescript application syntax use commas :

# only one parameter, it's fine
# EDIT : according to @vendethiel, it's not fine too :/
(x :: Map Int, Int) ->

# multiple parameters, commas confusion :/
(x :: Map Int, Int, y :: Int) ->

# we need use parens to fix this ...
(x :: (Map Int, Int), y :: Int) ->
# or
(x :: Map(Int, Int), y :: Int) ->

# with haskell-like application syntax it's better but it's out of scope for this issue
(x :: Map Int Int, y :: Int) ->

Type ascription

I prefer :: over @: too. But adding more space-based operators isn't that great ...
We should remove :: as a shortcut for prototype. (I'm only half ironic)

Return type

Proposed return type () :: Int -> looks fine.

Type parameters

I prefer the precircumfix brackets [A] (x :: A) -> over the precircumfix parens with a custom separator (A) ::> (x :: A) ->.

Higher-order function

I didn't see any example with them :/

# something like that ?
(f :: ((x :: Int) :: String), x :: Int) :: String ->

@vendethiel
Copy link
Contributor Author

vendethiel commented Jan 18, 2018

generics

of importance: we could [probably] have (T, F)(a :: T, b :: F) -> if we wanted it.

HOF with spaced type application

It's true i forgot to mention this. Mostly because I don't have an actual "good" idea.

Higher order type: (f :: (Map Int, Array(Int))) :: Int ->
Higher order func: (f :: ((Map Int, Array(Int) :: Int)) ->

I don't really like it. TS uses => to indicate return type of function. It'd be easier if we had scala-like FunctionN...

HOF with []

Higher order type: (f :: Map[Int, Array[Int]]) :: Int ->
Higher order func: (f :: Map[Int, Array[Int]] :: Int) :: Int ->

HOF with a special case

I like how this one looks, tbh, but it's a special case.

We could have (f :: Function(Int, Int, Int)) -> to mean function(f: (Int, Int) => Int).
Or with my proposed syntax, f :: Function[Int, Int, Int].

@rhendric
Copy link
Collaborator

rhendric commented Jan 18, 2018

Type params

I prefer [A](x :: A) -> over (A)(x :: A) ->; there's less chance for ambiguity with the former. The reason I think a separating operator is a good idea is because right now, the lexer reaches backwards from the -> all the way to the opening ( of the parameter list, and that's bad enough. I don't want to make that operation even less local by making it extend backwards further to capture another set of brackets of any type. If [A](x :: A) -> can be done only by changing how ] is lexed—say, when it is immediately followed by a no-space (—then I'm more okay with it.

What about something like forall A, (x :: A) ->? A feature this big might be worth a keyword or two. That also makes generic types that don't involve functions possible: forall A, [A, A], for example, the type of homogeneous pairs. And this is very left-to-right-parsing friendly.

HOF and return types

I don't like the double :: for higher-order functions—much better if we can use an arrow the way I think both Flow and TypeScript do (and again, let's reuse things that already exist in LiveScript). But then this leads to precedence problems if we are annotating return types the way it has been previously proposed:

map = forall (A, B), (f :: (A) -> B, arr :: Array A) :: Array B -> ...
# or
map = [A, B](f :: (A) -> B, arr :: Array A) :: Array B -> ...

If :: has higher precedence than ->, then f :: (A) -> B won't work; but if it has lower precedence, then the return type of the function will look like Array B -> ... instead of Array B.

I already had issues with that notation for return types because, again, it extends the non-locality of the -> lexer rewriting—the lexer has to reverse-parse a type, but only if that type is preceded by a ::, all before reverse-parsing a parameter list—ugly! I think we need some other alternatives for return types, perhaps following the -> instead of preceding it. Not sure what that should be though.

Flow stuff

Yes, in Flow, { foo: ?SomeType } is a field with a nullable (or undefined-able) type, and { foo?: SomeType } is an optional field. Flow cares about the difference because it changes whether 'foo' in obj is a discriminator for determining if obj is of that type.

* is what Flow calls the ‘existential type’. It's a fill-in-the-blank request for the type checker—i.e., something (that isn't any or mixed) should go here, you figure out what.

@determin1st's suggestion

Supporting multiple syntaxes, however you do it (through in-source pragmas like what you suggest, configuration files, or compiler flags), exponentially increases what the language maintainers need to support. LiveScript's one syntax is barely being maintained right now; I am strongly against dividing our efforts further unless there is a renaissance in people contributing to the code.

Classes

We should also think about these. LS classes don't create ES6 classes, so they should be typeable as (constructor types) -> { all declared members in the class }. I guess, for starters, that could be done manually with a var declaration above the class declaration, but that's pretty bad long-term due to the redundancy. But if we want to do this the right way, we also need syntax for annotating the types of properties in objects, since that's how LS classes are declared. member :: Type: value is shorthand for member :: { Type: value }, so that syntax is out. We could maybe special-case (member :: Type): value, if we're sure we'll never support something :: Type as an expression? Or we need some other idea.

@vendethiel
Copy link
Contributor Author

Type params

What about something like forall A, (x :: A) ->? A feature this big might be worth a keyword or two. That also makes generic types that don't involve functions possible: forall A, [A, A], for example, the type of homogeneous pairs.

LS tends to follow Coco in the "Fewer keywords" mantra.
It's a bit weird because we're really ditching the likes of C++, Scala, TypeScript, etc, while going for a Haskell-like approach, where they separate the type signature and the body.

And this is very left-to-right-parsing friendly.

Well, forall (A, B) -> currently parses relatively cleanly (ID:forall ( ID , ID ), though it gets weirder after rewriting...), so it might be doable. I still prefer getting a syntax that resembles Flow/TS that we are targeting, though.

HOF and return types

I don't like the double :: for higher-order functions—much better if we can use an arrow the way I think both Flow and TypeScript do (and again, let's reuse things that already exist in LiveScript). But then this leads to precedence problems if we are annotating return types the way it has been previously proposed:

If we do that, we can just able forget the idea of doing this properly in the parser and not in the lexer, methinks... Also then it becomes weird that the function's own return type does not use that keyword (though it's the same as in TS, e.g. function(f: int => int): int).

I think we need some other alternatives for return types, perhaps following the -> instead of preceding it.

I don't think so:

  • Make it look weird considering our current implicit block (-> 5 vs -> :: Int => 5 – or something)
  • Make the lexer/parser have to gobble N tokens, probably until a newline (-> Int & Map Int, String).

Flow stuff

Yes, in Flow, { foo: ?SomeType } is a field with a nullable (or undefined-able) type, and { foo?: SomeType } is an optional field. Flow cares about the difference because it changes whether foo in obj is a discriminator for determining if obj is of that type.

I see. There's no such difference in TS AFAIK. I find it really ugly, but hey. We havn't even started to bikeshed on how to type heterogeneous objects yet.

  • is what Flow calls the ‘existential type’. It's a fill-in-the-blank request for the type checker—i.e., something (that isn't any or mixed) should go here, you figure out what.

Ok. Thankfully, we already have a placeholder thingie, which is what the ML family also uses: _.

Classes

See the comment on heterogeneous objects. They fall under the same category I'd say.

We could maybe special-case (member :: Type): value

Note that this is not enough.

class C
  (a :: Array Int): []
  -> @a.push 3
console.log C.new.a # prints [3]
console.log C.new.a # prints [3, 3]

@rhendric
Copy link
Collaborator

Note that this is not enough.

I don't understand that last example at all. More words, please?

@vendethiel
Copy link
Contributor Author

vendethiel commented Jan 18, 2018

We need a way to type instance variables, not only prototype variables.

class C
  -> @a :: Array Int = 5 # we need a way to say "this will always exist on the object"

Are you available on IRC for a few minutes?

EDIT: IRC logs:

https://gist.github.com/vendethiel/5b830f94b5cf838e66c61ea629e31d0c

[21:56:36]  <rhendric>	I think we've agreed that both TS and Flow should be targets; that we want to support everything possible eventually; and that a pure Haskell-like approach isn't viable (but maybe something that is almost that approach with some very simple infix :: support is?).
[21:58:59]  <rhendric>	I personally am willing to give up a fair amount of visual appeal in order to have the lexer not get more complex.

We disagree on how things should look and what the syntax should be (and the "complexity budget" we have). Though we agree on all the pain points we see: typing objects, typing classes, typing tuples, typing function as parameters.

@rhendric
Copy link
Collaborator

I wrote my own proof of concept, with tests (I've checked the resulting code on typescriptlang.org; looks good as an initial stab). See 68e836267.

It's very hacky also, but all the hacks are in ast.ls instead of lexer.ls. Notable bikeshed decisions made here:

  • Spaced :: for has-type
  • Two ways to annotate function types: as a single large unit by annotating the entire function expression, or by annotating the individual parameters and the last line of the function body (somewhat unconventional, I know, but works well for one-liners)
  • Type parameters declared using a forall ‘type’ which gets special handling in ast.js (it lexes just like a regular identifier, and so doesn't need to be a reserved keyword elsewhere in LS code)

@vendethiel
Copy link
Contributor Author

Nicely done!

@vendethiel
Copy link
Contributor Author

I'm gonna go full crazy for a bit... @rhendric first proposed Haskell-style annotations:

map :: forall A, B, (Array(A), (A) -> B) -> Array B
map = (as, f) -> as.map(f)

mapper :: Int -> Int -> Int 
mapper = (x) -> (y) -> x + y
map [] mapper

I think this is impractical as it means you need to declare a new variable for every function.

I want to mention a way this could be solved using a Haskell-style where (...which we add a few years back), if made a bit smarter:

map [] mapper
  where mapper :: Int -> Int -> Int
             mapper = (x) -> (y) -> x + y

would not bother me, if we made where recognize variables that are mentioned only once, and did literal replacements.

@ozra
Copy link

ozra commented Jan 21, 2018

@vendethiel, I should have been more clear as to my intention with the examples. They were more of what would work without clash with the simple definition "after :: comes type expression". They were actually not meant as actual desired syntax. Sorry about the confusion.

vendethiel: my goal is to only allow this in let, var, and const.

[var decl] - That sounds preferable to me too.

vendethiel: probably, but with a keyword. Also, no <>.

[type alias] - I fully agree here too, except <> or [] are fine by me.

vendethiel: maybe? it currently parses but doesn't do anything.

["casting"] - I figure it can't harm to allow the notation should some one find a good use for it for a static analysis tool or such. If TypeScript is considered the specific target for LS type notation (which I would find perfectly reasonable), then of course any notation that won't forseeably be introduced, or already exist, in TS would maybe be confusing.

vendethiel: what's that supposed to be?

[class declaration] - Based on the already crazy example of type alias, this would simply signify a class declaration. But again, not wanted in practice.

I just prefer the idea proposed of spaced :: over @: etc. variations.

Regarding generics, I much prefer the Foo[Bar, Zoo] over Foo(Bar, Zoo). Much clearer separation of distinct concepts. I could go into a lengthy motivation here concerning the human language processing unit and the need for redundant information (in contrast to my earlier examples), but... when someone's prepared to put in the time to implement type annotations, I'm happy regardless of the syntax in the end.

@vendethiel
Copy link
Contributor Author

vendethiel commented Jan 21, 2018

[type alias] - I fully agree here too, except <> or [] are fine by me.

I've always prefered matching brackets, but fine.

then of course any notation that won't forseeably be introduced, or already exist, in TS would maybe be confusing.

we could have it mean TS' varname as Type.

I just prefer the idea proposed of spaced :: over @: etc. variations.

as @rhendric just demonstrated - doable, but requires a bit of juggling.

Regarding generics, I much prefer the Foo[Bar, Zoo] over Foo(Bar, Zoo). Much clearer separation of distinct concepts. I could go into a lengthy motivation here concerning the human language processing unit and the need for redundant information (in contrast to my earlier examples), but... when someone's prepared to put in the time to implement type annotations, I'm happy regardless of the syntax in the end.

Knowing what people would prefer using is still helpful. Thanks for the input.

@vendethiel
Copy link
Contributor Author

vendethiel commented Jan 30, 2018

microsoft/TypeScript#21316
microsoft/TypeScript#21496

TypeScript seems to be moving... in many directions at once.

How the hell are we writing type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T; in LS, except for

``type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;``

@rhendric
Copy link
Collaborator

I'm not at all sure this is a good idea, but continuing in the vein of my proof-of-concept, the below comes remarkably close to compiling:

type ReturnType = forall T, if T extends ((...Array any) -> infer R) then R else T

@vendethiel
Copy link
Contributor Author

I'm not at all sure this is a good idea, but continuing in the vein of my proof-of-concept, the below comes remarkably close to compiling:

If anything, I think I'd prefer it if we had a Type-like class that'd be basically a pattern matcher where your AST hack were extracted to, that'd recursively rewrite calls and al to types.

@rhendric
Copy link
Collaborator

rhendric commented Feb 6, 2018

That would be a clean way to encapsulate things. However, I think I'd want to avoid a proliferation of TypeFun, TypeObj, etc. classes, duplicating the existing AST class family. And I want to reiterate that existing shorthand notations (foo: string == { foo: string }, etc.) should work as well in type expressions as they do in value expressions, and a lot of that code lives in the existing AST classes. So that's a thing to work around.

@vendethiel
Copy link
Contributor Author

However, I think I'd want to avoid a proliferation of TypeFun, TypeObj, etc. classes, duplicating the existing AST class family.

That's not what I'd envisioned. The Type::compileNode would just generate JS from its content, recursively descending into its AST fragment.

@rhendric
Copy link
Collaborator

rhendric commented Feb 6, 2018

Could work! That approach might necessitate pulling some logic that lives in existing compileNodes out into their own methods, but that's not a bad thing.

@vendethiel
Copy link
Contributor Author

Could work! That approach might necessitate pulling some logic that lives in existing compileNodes out into their own methods, but that's not a bad thing.

Indeed, I'd argue that's a good thing, and untangling type/real calls code is the only sane way forward imho.

@vendethiel
Copy link
Contributor Author

@rhendric would you consider either 1) allowing type declaration beforehand, like my where example and/or 2) being able to declare the return type as -> :: x, which works better for non-single-line expressions (imho – because you can read the whole signature in one go)

@rhendric
Copy link
Collaborator

rhendric commented Mar 29, 2018

I'm open to both of those possibilities, depending on how they're executed. I do like the idea of (2) over (args) :: ReturnType ->. Might even be nice in one-liner form; something like (it :: any) -> :: string; it.to-string!. I'm neutral on where, but could see myself loving it (or hating it) after more consideration.

@vendethiel
Copy link
Contributor Author

Might even be nice in one-liner form; something like (it :: any) -> :: string; it.to-string!

Ah, but... obviously, as it currently closes implicit functions, it's non-viable. I don't think we can keep both syntaxes. -> :: Number is ambiguous (is number the return type, or the whole function type)

@rhendric
Copy link
Collaborator

rhendric commented Apr 3, 2018

Why is that non-viable? We'd just change the lexer to not close implicit functions at :: when the :: immediately follows the ->, and add a grammar production to Block to support type ascriptions appearing as the first line of the block. Then -> :: T would make T the return type, and (->) :: T would make T the whole function type.

@vendethiel
Copy link
Contributor Author

vendethiel commented Apr 3, 2018

Why is that non-viable?

Okay, not non-viable, sorry – I'm just biased against that syntax, and requiring extra parens makes it even less interesting to me.

@rhendric
Copy link
Collaborator

rhendric commented Apr 3, 2018

Aw, really? I'm starting to like it better than my original proposals. Having support for all of

fun-with-types = (a :: A, b :: B) -> :: C; ...
fun-with-types = (a :: A, b :: B) -> :: C
  ...
fun-with-types = (a :: A, b :: B) ->
  :: C
  ...

feels nice and uniform to me, and all of these seem more readable than

fun-with-types = (a, b) -> ... :: (A, B) -> C
fun-with-types = (a :: A, b :: B) -> (... :: C)
fun-with-types = (a :: A, b :: B) ->
  ...
  ... :: C

which were the hacks I first tried out.

@vendethiel
Copy link
Contributor Author

Aw, really? I'm starting to like it better than my original proposals.

I think I poorly expressed myself. I mean your original proposal (my, whole) -> function :: type is the one I find less readable. So I think we're on the same page :-).

@vendethiel
Copy link
Contributor Author

vendethiel commented Apr 3, 2018

and add a grammar production to Block to support type ascriptions appearing as the first line of the block.

Bah, I'm really starting to consider adding an Ascr node in ast.ls to support that. (if block.lines.0 instanceof Ascr then return-type = block.lines.shift!. It'd probably also be the place to extract the if o.in-type-expr code. Guess I'll just add a return type to Block or a flag in the meantime

EDIT: ok, I have a patch that uses add-type on Block for now...

@rhendric
Copy link
Collaborator

rhendric commented Apr 3, 2018

I pushed a commit to my proof of concept branch supporting -> ::-style function annotations. I kept support for the more awkward alternatives, because barring another syntax abuse innovation, forall is still needed to declare type parameters.

@vendethiel
Copy link
Contributor Author

because barring another syntax abuse innovation, forall is still needed to declare type parameters.

Is this not acceptable for some reason? Obviously a breaking change, but...

map = forall A, B, (f :: (A) -> B, arr :: Array A) -> :: Array B
  arr.map f

@rhendric
Copy link
Collaborator

rhendric commented Apr 4, 2018

Is this not acceptable for some reason?

Technically, that could probably be made to work. Aesthetically, I'm biased against it because of how freely it mixes type and value symbols without clear syntactic dividers between them (::). Instead of knowing that forall is a special type constructor, but basically the same sort of thing as Array or Map in type expressions, you would now have to know that forall is a special language keyword that introduces type variables into the scope of a child value expression, which is quite its own beast.

@vendethiel
Copy link
Contributor Author

you would now have to know that forall is a special language keyword that introduces type variables into the scope of a child value expression

That ship has sailed already, though, no? even when writing -> o :: forall A, B, ..., forall is special because any other identifiers would result in a parameterized type

@rhendric
Copy link
Collaborator

rhendric commented Apr 4, 2018

Maybe I'm splitting hairs, but to me the difference seems large: forall as a type constructor is special, yes, but special as a type constructor. To a reader, it behaves syntactically like a type constructor—valid in a type expression, and its children are type expressions. It's only special semantically. forall as a keyword in a value expression is a unique construction. You can't think of it syntactically as a function, because most of its arguments are types; and it isn't syntactically a type constructor because it overall is a value, and so is its last argument. It'd be the first special form that works with types, besides ::. I'm hoping to avoid introducing such forms, because (given my preference for reusing value expression syntax for type expressions) I think it's going to be important to be very clear when you're looking at a type and when you're looking at a value. If :: is the universal this-is-a-type indicator, great. If we add more things like keyword forall that can indicate types, I think that's a big step towards anarchy and confusion.

@rhendric
Copy link
Collaborator

rhendric commented Apr 4, 2018

I should also add that I expect there to be more special type constructors; see, for example, infer from several comments up. (That one is only special in that it would compile to infer T instead of infer<T>, but still.)

@vendethiel
Copy link
Contributor Author

vendethiel commented Apr 4, 2018

I should also add that I expect there to be more special type constructors; see, for example, infer from several comments up. (That one is only special in that it would compile to infer T instead of infer, but still.)

Fair enough, but I don't see any other that need to be an introducer. Maybe if Flow or TypeScript start supporting existential qualifications, but...
I see your point though. We're making a construct that "starts outside" the type part, and wrap it back into it. That's a precedent... But clearly I don't want a different syntax to introduce type parameters.

There's no real "good" place, except if we want to... pluck them out as we're reading it (basically like TS's infer works/will work): rewrite (f :: (forall A) -> forall B, xs :: Array B) -> :: Array B. That way we never mix type-level and value-level, but I really don't like the way it looks, it's a bit awkward, and it makes reading the type signature harder (imho).


I'd appreciate if a few more people voiced their opinions, or threw in some other ideas! :-) @ozra , @wryk , or from #803(?) like @robotlolita, ... yada

@rhendric
Copy link
Collaborator

rhendric commented Apr 4, 2018

(Starting to wonder if perhaps [A, B](a :: A, b :: B) -> ... is the least of all evils after all...)

@vendethiel
Copy link
Contributor Author

Surely you mean the least forall evils.. :-P


I'm totally OK with precircumfix [], as I championed at the beginning.

Is there actually a case where we want it inside a type signature? (a :: [B](...)) ->? not sure offhand, but I'd say no.

@rhendric
Copy link
Collaborator

rhendric commented Apr 4, 2018

Why shouldn't it be valid there?

@rhendric
Copy link
Collaborator

rhendric commented Apr 4, 2018

In the precircumfix [] world, disambiguating between these cases would be tricky, I think:

  • o[x](a) -> b
    • tokens: ID:o DOT:. [ ID: x ] CALL(:( ID:a )CALL:) CALL(: PARAM(: )PARAM: -> ID:B )CALL:
    • compiles to: o[x](a)(function(){ return b; });
  • o [x](a) -> b
    • tokens: ID:o CALL(: GENERIC_FUN: [ ID: x ] PARAM(:( ID:a )PARAM:) -> ID:B )CALL:
    • compiles to: o(function<x>(a) { return b; });
  • o [x](a)
    • tokens: ID:o CALL(: [ ID: x ] CALL(:( ID:a )CALL:) )CALL:
    • compiler error (or, would compile to: o([x](a));)

The extra PARAM(: )PARAM: pair gets inserted early in the lexer, so either a post-tokenize rewriter has to deal with that (in addition to moving one CALL(: and rewriting existing CALLs to PARAMs), or the initial tokenization has to be made more complicated to keep track of where the [] starts and whether there's a space before that (keeping in mind cases like [[T](a :: T) -> ...]).

@vendethiel
Copy link
Contributor Author

vendethiel commented Apr 4, 2018

Why shouldn't it be valid there?

I wasn't sure fn<A>(a: <B>(x: A) => B): <B>(x: A) => B was actual working syntax. Nice!


o[x](a) -> b
o [x](a)

For both... We only need to look a single token (]) back for type parameters after we're done tagging PARAM( and )PARAM in Lexer::parameters, no? In this case, we'd look back, see CALL( and stop there.
Is there an order issue I'm not taking into account?

@rhendric
Copy link
Collaborator

rhendric commented Apr 5, 2018

The point is that whether or not those parentheses should become PARAMs or CALLs depends on whether there's a space before the opening [. With the lexer as it is, they're assumed to be CALLs since they start adjacent to ], and Lexer::parameters will ignore them and introduce new PARAM( )PARAM tokens. But suppressing interpreting ( as CALL( wouldn't work because it depends on whether there's going to be a -> later. So Lexer::parameters has to look all the way back to [, or we need to track some fiddly state about the last [ seen. Or fix the whole thing in a fiddly rewriter.

There could very well be a simpler answer—don't let me stop you looking for one. I just tried and that's what I ran into before I gave up for now.

@danielo515
Copy link

Hello,

I'm probably late to the party, but there is at least one thing I want to say: please use HMS type annotations, or something as close as possible to Haskell. One of the reasons I hate all those typed languages is because all the noise and weird characters their require. They make the code verbose and hard to read and to reason about it. That's why I hate java, that's why I like javascript and that was one of the reasons why Haskell impressed me a lot: Types are possible without visual clutter.

@vendethiel
Copy link
Contributor Author

vendethiel commented Sep 11, 2018

Coming back to this (I'd like to have it for 1.7, if that's imaginable...)

regarding [x,y]() ->, it's currently an "invalid callee" error, so even if it's tough to fix in lexer (rewriter could probably go back to [, find if the previous is a CALL(, that means it's spaced... Though it also needs to handle myfun = []() ->... Can we just filter out DOT?)

  • maybe with a grammar production? TypeList CALL( ... )
  • in the nodes, if we have a Chain with a Arr followed by a Call (but we need to rewrite this to a signature...).

From what we discussed earlier, it seems we don't need a special syntax for infer which can be littered wherever the type variable is used.

@rhendric
Copy link
Collaborator

By all means, if you have an approach you think will work, give it a shot. I don't think I have any more helpful thoughts to offer you about the []() -> problem—I'm a little pessimistic about the possibility of wedging that syntax into the parser we have, so for now I'm more excited about spending my free hacking time working on an entirely new parser, which may form the basis of a successor language. But I will still gladly review and offer feedback on patches if you write them.

@vendethiel
Copy link
Contributor Author

vendethiel commented Sep 13, 2018

OK. I tried for a bit, and the AST is a bit too damaged by the time we get to Chain in the AST. (the pattern is Chain [Arr with generics; Call with the params; Call with the actual function definition].
Restoring it is definitely possible, but it's ugly.
I have a patch that implements [] ::> as the option you had suggested, and one that implements value-level forall.

$ lscc 'f [A, B] ::> (a :: A, b :: B) ->'
f(function<A, B>(a: A, b: B): any{});

$ lscc 'f = [A, B] ::> (a :: A, b :: B) ->'
var f: <A, B>(arg0: A, arg1: B) => any;
f = function(a, b){};

I can take a stab at the Chain rewriting.

@rhendric
Copy link
Collaborator

Nice. Reviewing the thread, it occurs to me that my earlier discomfort with value-level forall also applies to my own proposal of ::>, so the only real difference between the two is readability and the consumption of a new keyword. If we do run with ::>, is there a reason to use square brackets there?

(Still kind of hoping one of us—or someone else—comes up with either an elegant implementation of []() -> (somehow...) or a new syntax proposal that doesn't suck.)

@Djeg
Copy link

Djeg commented Feb 1, 2019

Hello :). I'm late and probably out of subject but anyway. What about a type system inspired by the fantasy land specification and a bit of purescript ?

It could look like:

# var name: string = 'john'
let name :: String = 'John'

# var add: (a: number, b: number) => number;
# add = function (a, b) { return a + b; }
let add :: Number -> Number -> Number = (a, b) --> a + b

# multiline support and without curry
let sub
  :: (Number, Number) -> Number
  = (a, b) -> a - b

# Generic support:
let map
  :: a, b. (a -> b) -> Array a -> Array b
  = (f, arr) --> arr.map f
# var map: <A, B>(f: ((a1: A) => B), arr: Array<A>) => Array<B>;
# map = function (f, arr) => { return arr.map(f); }

# Interfaced generic support:
let map
  :: User a, b. (a -> b) -> Array a -> Array b
  = (f, arr) --> arr.map f
# var map: <A extends User, B>(f: ((a1: A) => B), arr: Array<A>) => Array<B>;
# map = function (f, arr) { return arr.map(f); }

For function keyword we could inverse the order:

function add x y :: Number -> Number -> Number
  x + y

Wich i think open the door to some inteligent pattern matching (i'm going too far ^^):

let addFiveToTen :: Number -> Number =
  | (10) -> 15
  | (a) -> a

By the way, very good work with livescript, i'm really enjoying it :)

@vendethiel
Copy link
Contributor Author

What about the lambdas?

@Djeg
Copy link

Djeg commented Feb 1, 2019

Good point, like do -> 3 + 4 ?
Maybe something like do :: () -> Number = -> 2 + 4 ?

I'm not sure to get a concrete use case of a lambda here, do you have any concrete example ?

@vendethiel
Copy link
Contributor Author

The whole thread goes into detail about our ideas. I know it’s fairly long but have you read all proposals?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants