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

So-called "next big version" #811

Closed
8 tasks
vendethiel opened this issue Jan 12, 2016 · 52 comments
Closed
8 tasks

So-called "next big version" #811

vendethiel opened this issue Jan 12, 2016 · 52 comments

Comments

@vendethiel
Copy link
Contributor

LiveScript "2.0"

"It's a-boot time"

So, this got talked about quite about before, but I'm not sure that's actually what's interesting to us now.

However, there's definitely a need to cleanup some things in LS, and especially with ES6/7-related.

Are people okay with breaking changes now? If they're not, we can discuss what we want to change, but in the meantime:

  • Implement type ascriptions (PR starting Start working on type ascriptions #803)
  • ES6 support:
    • modules. @gkz proposed import! and export! for the ÈS6 modules (we need to decide between <<<{<,} and import {all,} anyway)
    • classes: I don't think we want them. LiveScript code tends to use invokable class bodies
    • async/await can be delayed a bit more, I think. It's ES7, and it's not quite there yet... We'd also need to decide on a syntax (eww ->**)
    • const needs to be compiled to ES6's const
    • I'm not sure we need to change let's meaning, I don't think it adds much.
  • Remove for ever
  • Parse the star as a real operator (not just void=)
  • Remove implements (unless people actually use it)
  • Remove the match-introduced operators >== etc
  • Make match an actual pattern matching operation (semantics TBD, that can come later)
  • Remove operator overloading (...or make the compiler aware of types...)
  • Trim down synonmys. Includes otherwise/default, case/when, =>/then (I guess this one is the one people like), etc

This will be controversial (at best), but I think it's worth discussing:
I believe we should remove implicit variable declaration. We have var, and := when we need to change a value.
I think the scoping rules would seem much saner if we had to explicitly declare those. But I know this isn't going to fly for a lot of people, so...

There's another controversial topic: .., it and that. I like what we currently have, but I'm too biased to know if that bugs people or not.

@gkz
Copy link
Owner

gkz commented Jan 12, 2016

Good idea, let's at least get everything down on a list.

@igl
Copy link
Contributor

igl commented Jan 12, 2016

Types are neat.. but does that mean there will be --compile-target es6/ts/flow?
I would like to see object-spread/rest on this list. <<</import would be obsolete.

++ es6 modules, const, match, it
-- implicit variable declaration, that and everything else!

@darky
Copy link
Contributor

darky commented Jan 12, 2016

In high priority ES features, which don't have analog in LS: modules, iterators, ...
Type ascriptions cool feature, which level up LS (many concurents have it already)

I think LS should be compile in ES with time. At the beginning it may compiled in two modes:

  • old, that used now
  • new, with flag ala --ecmascript

In new mode fat arrow, default params, spreads, destructing, e. t. c. compiled as is in ES. Later, when browsers allow, compile only in ES without flag.

LS classes better than ES now, because it have private members (without binding context) and class props. But if this features accepted in ES, LS should be compile classes in ES.

Variable declaration mechanism awesome in LS, I think it don't touch.

Each of removed features from LS should have own issue discussion.

@zloirock
Copy link

ES6 support

What about for-of / iterators?

@vendethiel
Copy link
Contributor Author

We have an issue and a branch for that

@zloirock
Copy link

I know it. I mean that in this list / next big version.

@unclechu
Copy link
Contributor

I think const and all "variables" in const LS mode should be compiled to const too in ES mode.'
UPD: Sorry, didn't noticed that it already in topic.

@unclechu
Copy link
Contributor

Agree with:

I believe we should remove implicit variable declaration. We have var, and := when we need to change a value.

I prefer to use const mode by default and use var/:= when I REALLY need variable.

@rightfold
Copy link

Implicit variable declaration in LS isn't as awful as in e.g. CoffeeScript where merely adding a global variable may change the behaviour of code elsewhere.

But I agree that full lexical scope (not function scope) and explicit declarations improve code legibility. It makes it clear when exactly a variable starts being in scope; small tokens like -> and do are easily overlooked.

@unclechu
Copy link
Contributor

@rightfold

CoffeeScript where merely adding a global variable may change the behaviour of code elsewhere.

And it's a main reason why I hate CS! It's awful, it will penetrate your ass with a lot of pain when you don't expect it.

You have file with 200 lines for example and you declare variable in 50 line, and in 150 inside some map or something like this you declare another NEW (as you think) variable, but what happened, why this variable on 50 line has this strange value? Oh, of course, because it's CS.

P.S. In LS you definitely sure that you won't change value out scope if you don't really want it with specific operator :=.

@darky
Copy link
Contributor

darky commented Jan 14, 2016

I like too := livescript behaviour and hate coffeescript, where you can unsafe rewrite variable in outer scope.

@copygirl
Copy link

Remove implements (unless people actually use it)

Happened to read this as I was preparing to create an issue. I'm using it. Is there something wrong with implements? I found it's useful for "implementing" EventEmitter:

class Game implements EventEmitter::

@AriaMinaei
Copy link

If you have to pick either from TS or Flow, and are not able to support both, definitely pick Flow. It's sound, unlike TS. Supporting both would be nice though.

I completely agree. I might be biased in favor of flow, but that's precisely because of the same reason that I prefer LS instead of JS. Both flow and LS come from a more functional mindset.

@igl
Copy link
Contributor

igl commented Jan 17, 2016

If you have to pick either from TS or Flow, and are not able to support both.

The syntax differences between Flow and TS are very minor and Flow reads TS definition files natively.

definitely pick Flow.

I wonder if the flow praising people here actually used flow?
Up until recently there wasn't any support for let and const and this was in development for more than 6 months.
Flow does not even compare to TypeScript in support for language features and adoption speed.

@vendethiel
Copy link
Contributor Author

... On the other hand, TS' type system is unsound.

@igl
Copy link
Contributor

igl commented Jan 17, 2016

Flow has :any right? :)

@rightfold
Copy link

@igl Yes, but it never infers it.

@summivox
Copy link
Contributor

I agree with @copygirl on implements EventEmitter::. I feel it's the canonical way to use EventEmitter.

Can anyone elaborate on why class should not compile to ES6 classes?

@vendethiel
Copy link
Contributor Author

@summivox because metaprogramming, mostly

@waynedpj
Copy link

👍 keeping implements, handy for mixins

@summivox
Copy link
Contributor

@vendethiel how about a banged version class! that forces compiling to livescript class, while normal class compiles to ES6 class when available? This agrees with require vs require!, typeof vs typeof!.

@rightfold
Copy link

What is the benefit of compiling to ES6 classes? As far as I am aware they do not provide any unique functionality.

OTOH I think you have to if you want to support Flow or TS.

@robotlolita
Copy link
Contributor

Remove operator overloading (...or make the compiler aware of types...)

Hm, LS had operator overloading?

@igl Flow has better type inference, and uses occurrence typing, so types for most idiomatic functions in JS are very natural in Flow, and impossible to write in TypeScript. And TS's type system is unsound wrt subtyping + parametric types, so even if your program type checks, it might still throw errors at runtime, Flow's type system is sound, so if it something passes the type checker, you're guaranteed that you'll get that behaviour at runtime.

@summivox ES2015 classes are sort-of a very stripped down version of LS classes (they have similar runtime semantics, but ES2015 classes are designed to be static declarations, which really makes them less useful than they could be), you don't really gain anything by compiling to them.

@rightfold
Copy link

@robotlolita some operators behave specially when passed literals.

Like with all special cases, this hinders refactoring.

<[ one two three ]> * \|      #=> 'one|two|three'

x = <[ one two three ]>
x * \|      # oops!

@vendethiel
Copy link
Contributor Author

I guess I dhould say "overloaded operators"

@rhendric
Copy link
Collaborator

rhendric commented Feb 1, 2016

I'm working on reform for operator spreading; discussion at #831. The upshot is to remove support for existing ‘implicit’ unary operator spreading over lists (because does typeof [] != typeof [0] sit comfortably with you?) and replace it with operator spreading via an explicit splat, supporting some new cases in the process:

new ...[circle, square] => [new circle, new square]
++...{level, max: max-level} => {level: ++level, max: ++max-level}
...player{strength, hp}-- => {strength: player.strength--, hp: player.hp--}
...player{strength, hp} += 5 => {strength: player.strength += 5, hp: player.hp += 5}

I have a proof-of-concept demonstrating that the parser only needs small changes to support this.
👍/👎? Transition period for continuing to support implicit spreads, or break it right away?

@dead-claudia
Copy link
Contributor

_Warning: long comment._


TL;DR: (probably the longest TL;DR ever known to man 😄)

  1. 👍 for ES6 classes by default. There are ways to work around their static nature.
  2. 👍 for ditching for ever and loop. Not common enough to warrant their own syntax.
  3. Neutral on async-await. I'm working on a library that almost renders it unnecessary.
  4. Private class methods can still work. They just need transpiled out. See 1.
  5. Wrapped class methods can still be transpiled out. See 1.
  6. 👍 for (x) ~> y compiling to an arrow function
  7. 👍 for let and some do blocks using blocks with let. Requires extra compiler caution, though.
  8. 👍 for for let ... using let for inner variables.
  9. 👎 for operator overloading. Too many gotchas currently. Neutral if types are checked first.
  10. 👍 for way to externalize helpers into common module. Some compilers already allow this.
  11. 👍 for match doing actual pattern matching.
  12. 👍 for no longer hoisting declarations, but a naïve implementation will cause massive migration hazards, especially with switch and match. This will require some compiler black magic to accomplish correctly.
  13. 👍 for gradual types, but the compiler should call that itself, and 👎 for a pre-decided opinion.
  14. 👍 for for ... infor ... of.
  15. 👍 for keeping it/that/etc., as long as that is lexically scoped (it's not currently).
  16. 👍 for trimming synonyms. I came up with some of these already mentioned.
  17. Re-introduce where but with Haskell-like semantics. Makes implicit switch more useful.
  18. 👍 on reforming operator spreading. It's currently inconsistent.

Out of all of these, the only things holding me back from using LS for almost everything is match doing actual pattern matching, where for more helpful local variable declarations, and synonyms interfering with variable names I use.


My 2 dollars for all this (little more than 2 cents here 😄):

  1. BTW, classes can extend regular functions. This is valid code, and it will not throw in an ES6-compliant engine:

    function Parent(foo) {
        this.foo = foo
    }
    
    Parent.prototype.get = function () {
        return this.foo + 1
    }
    
    class Child extends Parent {
        constructor(bar) {
            super(1)
            this.bar = bar
        }
    
        add() {
          return this.get() + this.bar
        }
    }

    I think it would be safe to make LiveScript use ES6 classes. If an ES5-style class is still needed for an API, you can still declare it very Lua-like (which I already often do in both JS and LS, since I like the simplicity and purity of it):

    Parent = (@foo) ->
    
    Parent::get = -> @foo + 1
    
    Child = (bar) ->
      Parent.call @, 1
      @bar = bar
    
    Child:: = Object.create Parent::
    Child::constructor = Child
    
    Child::add = -> @get! + @bar

    Also, if you still need ES5 as a compile target, the legacy behavior (1.x) can still work.

  2. for ever and loop can die. I doubt many people use the former, anyways, and both cases could be converted into while true. I don't think it's common enough anywhere to warrant its own syntax IMHO.

  3. Async/await could be useful, but I'm working on a library that does the same thing with callbacks ATM, and it'll effectively fufill that need for me, since it's made to work well with backcalls. It's not yet on GitHub, but it'll probably be within a few days.

  4. Private static members in LS classes can just be transpiled out as references inside a closure, and the class returned from that closure. Classes are already defined in a closure now, so little is changing here.

  5. Class members that are not just functions can be desugared to this (getters and setters are similar in this regard):

    # LiveScript
    class Foo
      bar: wrap -> baz foo, @x
    // compiled JavaScript
    class Foo {
        // Needed for type systems like Flow
        bar() {}
    }
    
    var desc$ = Object.getOwnPropertyDescriptor(Foo.prototype.bar, "length")
    desc$.value = ref$.length
    Object.defineProperty(Foo.prototype.bar, "length", {
        configurable: true,
        enumerable: false,
        writable: true,
        value: wrap(function () {
            return baz(foo, this.x)
        })
    })

    Note that the only caveat with this approach is that methods are no longer callable.

  6. Bound functions (i.e. ~>) are arrow functions. Anything to the contrary is unexpected. Although that's a functional break from 1.x when called as a constructor, it's a clear mistake to do that IMHO (Function::bind can lead to a gotcha if you're relying on this and it's called as a constructor).

  7. let blocks should use actual blocks with let. do -> blocks can be similarly optimized if this isn't referenced. References for each new value should be generated for each argument to assign later, to safeguard against the TDZ.

  8. for let should use let. When the index is used, a separate reference should still be used because it's not safe within the block.

  9. I don't usually use operator overloading because IMHO the shorthands within it are hardly clearer than a method call. I don't see any use without a type-aware compiler.

  10. Some of the helpers (e.g. create$, import$, repeat$, indexOf$, slice$, etc.) should just alias the ES5 equivalent (respectively Object.create, Object.assign, String::repeat, Array::indexOf, Array::slice, etc.). Also, these should be able to be externalized like what Babel and a few other compilers do.

  11. match should most definitely do complete pattern matching. Efficient pattern matching in dynamic languages isn't unheard of.

  12. I wouldn't mind if all implicit declarations turned into let, but I fear a major migration hazard if this is implemented naïvely. If they were always declared with let and use the TDZ to their advantage (switch and match will have to compile to if-else in these cases at least), then it might work, since the variables are scoped to that case and all following cases.

    # The compiler would have to protect against this error:
    f = (x) ->
      # ReferenceError here
      | not x? and status != 10 => 'hi'
    
      # `status` defined here
      | status = getServerStatus x => "status: #status"
    
      # `status` defined here
      | otherwise => status `assert.equal` 0; 'whatever'
  13. If LiveScript becomes gradually typed, it should check them itself (using TypeScript or Flow internally), using temp files where necessary and remapping the files, and then emitting a compiled (and maybe optimized) format. Support for both (even if made to conform to some interface) would be preferred, with the user choosing and supplying a checker for them. TypeScript and Flow both have their strengths (Flow has a safer type system, but TypeScript is more flexible).

  14. for ... in should compile to for ... of.

  15. I'm fine with it/that/etc. Just keep that lexically scoped with let (it's currently not, due to ES5 limitations).

  16. I'm fine with trimming synonyms. I suggested gutting a few mentioned here myself in another issue. Also, can we drop some of the equality synonyms (is/== and isnt/is not/!=)?

  17. Re-introduce where with Haskell-like semantics. It would make implicit switches much more useful by making it easier to create relevant variables to use in each case. Just as an example of what I mean:

    # Without `where`
    awaiter = (func) ->
        end = &length
        len = end - 1
        ret = &[len]
    
        if &length == 0
            throw new TypeError 'Expected await to have a callback'
    
        if typeof &[len] != 'function'
            throw new TypeError 'Expected await to have a callback'
    
        args = new Array len
        for i from 1 til len
            args[i - 1] = &[i]
        args[len - 1] = bind callNext, task1 ret, @
        queue.push task2 initialize, @, taskThis func, undefined, args
        defer pull2
    
    # With where
    
    awaiter = (func) ->
        | end == 0 or typeof ret != 'function' =>
            throw new TypeError 'Expected await to have a callback'
        | otherwise =>
            args = new Array len
            for i from 1 til len
                args[i - 1] = &[i]
            args[len - 1] = bind callNext, task1 ret, @
            queue.push task2 initialize, @, taskThis func, undefined, args
            defer pull2
        where
            end = &length
            len = end - 1
            ret = &[len]
  18. I am in support of operator spreading, but I don't have any specific comments here to make.

@summivox
Copy link
Contributor

I agree that ES6 let should be carefully and thoroughly integrated into the language.


for ... in should compile to for ... of

👎

  • for value, index in array by 2
  • for in compiles to "plain old index loop" since Coffee
  • ES6 for of is mostly for generic iterators anyway and @gkz had it covered

@vendethiel
Copy link
Contributor Author

I'm on my phone, so not gonna answer everything right now, but in the case of types, no, LS should not call them itself. It makes no sense.

@rightfold
Copy link

Gradual types are asbestos.

@rhendric
Copy link
Collaborator

One more for the pile: is this feature worth keeping?

(f and g) x # compiles to f(x) && g(x)

It's always struck me as very ad-hoc.


for ever can die, but I'd be (slightly) sad if any of the suggestions to remove/change loop, is/is not, yes/no, for in, it, or that took effect, all of which I feel can improve readability over their alternatives when used judiciously.

@rightfold
Copy link

@rhendric (f and g) x is related to overloaded operators as discussed above and in the original post. Some operators behave differently when used in certain syntactic constructions.

@rhendric
Copy link
Collaborator

Fair enough—I hadn't generalized ‘operators that behave differently when used on certain literals’ to ‘operators that behave differently when used in certain syntactic constructions’, but it's clearly related. Possible that the decision to keep/remove would not be uniform across that entire class, though—each case has its own implementation to maintain, after all—so it might be worth enumerating somewhere all the cases under discussion.

@vendethiel
Copy link
Contributor Author

In this case, it was (mostly) created for match

@rhendric
Copy link
Collaborator

That makes sense; outside the context of a match pattern, though, perhaps it should either revert to the default operator behavior, or behave more like other overloaded operators (in that they require operand syntax trees that would be useless otherwise to trigger the special behavior)? For the latter route, I'd specifically propose as the trigger requirement that both operands are function expressions of some sort—it would admittedly be pretty cool to be able to say

first-quadrant-points = filter (.x >= 0) && (.y >= 0)

Lifting the or in (f or g) x outside of a match is a little questionable to me; I expect it to mean almost the same thing as (f ? g) x, which is a useful thing to say. And now that I know that the feature stems from match patterns, I can go even deeper into the rabbit hole: (25 or 50) x is a valid expression outside of a match too! I never knew!

@rightfold
Copy link

I'd prefer more code (e.g. lift (&&), (.x >= 0), (.y >= 0)) over special behavior.

@vendethiel
Copy link
Contributor Author

Yeah, let's not add literal overloading anymore :)

@dead-claudia
Copy link
Contributor

I agree with literal overloading. In my experience, it's a better obfuscation tool than anything useful in most cases. And the gotcha/inconsistency with the dot operator (( . ) fails, but previously compiled to composition, and (.) has always been member access) is why I rarely use it now. I couldn't tell you how many minor bugs I've run into with the inconsistency between the two, especially with partial application, where everything falls apart. I prefer >> for the same reason so many Haskellers use >=> (a reversed version of their .), but wouldn't mind if . for function composition died.

@vendethiel
Copy link
Contributor Author

I'd like getting people's opinions on #863.

@dead-claudia
Copy link
Contributor

I would like to add to that grand list making some of the unusual edge cases that should never parse as explicit errors instead of weird behavior or compiler runtime errors. Things like (object.foo _ .prop) or (foo = ...args) ->. One case, (...a=b) ->, is being fixed in #863.

@dead-claudia
Copy link
Contributor

dead-claudia commented May 7, 2016

@gkz

Have you thought of making classes this way in LiveScript?

# Initial
class Foo extends Array
  ->
    super ...
    @prop = 1

  foo: -> 1
  bar:~
    -> @prop + 1
    (value) !-> @prop = value - 1
// Compiled ES6
function defineMethods$(C, Super, props) {
  C.prototype = Object.create(Super.prototype);
  for (var prop in props) {
    var desc = Object.getOwnPropertyDescriptor(props, prop);
    if (desc) { // own property
      desc.enumerable = false;
      Object.defineProperty(C, props, desc);
    }
  }
}

let Foo = (Super => {
  function Foo(...args) {
    // Note that `this` must be aliased.
    let this$ = Reflect.construct(Super, new.target, args);
    this$.prop = 1;
    return this$;
  }

  defineMethods$(Foo, Array, {
    foo() { return 1 },
    get bar() { return this.prop + 1 },
    set bar(value) { this.prop = value - 1 }
  });

  return Foo;
})(Array);

@Announcement
Copy link

I like .. and it but I've never used that, actually didn't even know about it until reading this post but it seems nice.
Other than that, the other things I could personally care less about.
that's just my two cents :D

@dead-claudia
Copy link
Contributor

@vendethiel WDYT about my solution to the class problem above?

@vendethiel
Copy link
Contributor Author

Not sure why you'd want this kind of solution, tbh. We can probably compile the method names we know at compile-time into a real ES6 class

@dead-claudia
Copy link
Contributor

dead-claudia commented May 19, 2016

Good point. So, in other words, it'll be a combination of class F {} and Object.definePropert{y,ies}?

The rationale for my idea was that you could both call and construct it for similar effect (unless you decide to include new.target or similar hackery), and it would both be subclassable and be able to subclass builtins.


I'm not as invested in LiveScript as I used to be, between the incorrect mappings (too busy to track them down) with source-map-support and because I've grown tired of all the transpiling and tooling fiasco. It's similar to JavaScript fatigue in that respect...I'm not anti-tooling; it's just that a bit too often in my experience, tools get in the way.

It doesn't help my energy that, for the testing framework I'm currently working on and putting most of my effort into, I've had to reimplement and drop about 2-3 different dependencies, and copy portions of other libraries. One made debugging a difficult bug even harder because of a dependency chain completely consisting of ~5 lines of code each. Another was 90% what I wanted, but it didn't have an option to fix the 10%. Mocha had about ~80 lines of code I needed, but I had to tweak them for a different code architecture. Since this is 3.7k LOC (7.4k for tests, mocks, etc.) pure ES5 JavaScript (I bailed from LiveScript early on because the stacks were wholly unhelpful at the time), I haven't done much with LiveScript, and generally haven't had the time.

@chrisvfritz
Copy link

Any chance removing this feature could be added to the list?

@dead-claudia
Copy link
Contributor

That one's bitten me before, too. It's annoying.


By the way, what drove me away from LiveScript after really wanting to love it was all the little things:

None of these would've individually drive me away, but a buggy compiler and language inconsistencies like these among others are what would bring me back for 2.0. Yes, pattern matching is nice, but it's the thousand papercuts that killed it for me.

@JoyKrishnaMondal
Copy link

For the first one I just do this - to make it more explicit

obj1 = [
    *id: 1
]
obj2 =
    * id: 1
    * id: 2

I know it could be better but I will wait for it when it updates.

@JoyKrishnaMondal
Copy link

One think I think would make LiveScript really shine is the ability to create custom operators

so for example with fantasy-land I would like to do

hello = (x,y) --> x + y 
hello =  _.curry (x,y) -> x + y 

instead of having livescript compile a curry function for every script.

also for monads

do 
<- hello x
console.log 'foo' 
(hello x).bind (-> console.log 'foo')

this gives a lot of flexibility and prevents a lot of arguments ( like human arguments not computer arguments ) - and also makes live-script flexible to changes from the rest of the javascript world.

@vendethiel
Copy link
Contributor Author

Sorry, that's not something I'm gonna work on on the foreseable future. If someone wants to take this list over and do something with it, best of luck to you & to the future of LS.

@celicoo
Copy link

celicoo commented Oct 3, 2016

I'm very interested in making this go forward, is there anyone else?

@dk00 dk00 mentioned this issue Dec 28, 2016
@willin
Copy link

willin commented Aug 27, 2017

mark

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