gkz / LiveScript

LiveScript is a language which compiles to JavaScript. It has a straightforward mapping to JavaScript and allows you to write expressive code devoid of repetitive boilerplate. While LiveScript adds many features to assist in functional style programming, it also has many improvements for object oriented and imperative programming.
http://livescript.net
MIT License
2.31k stars 155 forks source link

Typings bikeshedding #999

Open vendethiel opened 6 years ago

vendethiel commented 6 years ago

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:

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 commented 6 years ago

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

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

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):

vendethiel commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

in LS, we have:

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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:

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 commented 6 years ago

Note that this is not enough.

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

vendethiel commented 6 years ago

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 commented 6 years ago

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:

vendethiel commented 6 years ago

Nicely done!

vendethiel commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

[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 commented 6 years ago

https://github.com/Microsoft/TypeScript/pull/21316 https://github.com/Microsoft/TypeScript/pull/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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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

vendethiel commented 6 years ago

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 commented 6 years ago

Why shouldn't it be valid there?

rhendric commented 6 years ago

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

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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?)

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 commented 6 years ago

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 commented 6 years ago

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.