ethereum / moon-lang

Minimal code-interchange format
MIT License
193 stars 20 forks source link

Bang notation vs pattern matching. #31

Closed Kesanov closed 6 years ago

Kesanov commented 7 years ago

It looks like | a =<(prompt "How are you?") could be simply replaced with | a = prompt "How are you?" where the second version is using simple pattern matching in let.

The big advantage is, we can pattern match on multiple arguments, and we don't need < and > anymore (and it is IMHO easier to understand):

(| a b = tuple 2 10 20
 | c = tuple 1 30
 d = 40
 add a (add b (add c d))) -- returns 100

The disadvantage is you have to write | _ = print "Hello" instead of (print "Hello")> and you cannot use ... <foo ... anymore, which might be a good thing, since it is really hard to understand code that contains multiple binds in one line.

VictorTaelin commented 7 years ago

Interesting. I like the idea of not needing <, and giving up of > is a reasonable tradeoff for that. Looks like something that could work, but I'm not sure I fully get it. I need more info to really understand what you did there. Could you be precise about the translation you have in mind?

For example, the current notation does this: for every <foo, move foo to the nearest binder or pipe, wrap everything else in a lambda and replace the old location by the bound variable of that lambda. For example, | (if 1 [<foo <bar 3] 4) becomes (foo X => (bar Y => (if 1 [X Y 3] 4))). I kinda like this syntax because it:

1. Is, together with let, at least as expressive as the do notation. Ex:

foo = do
  print "Write a line."
  line0 <- getLine
  print "Write another line."
  line1 <- getLine
  if length line0 < length line1
    then print "The first line was shorter."
    else print "The second line was shorter."
  return "Done."

becomes

foo = | 
  (print "Write a line.")>
  line0 =< getLine
  (print "Write another line.")>
  line1 =< getLine
  (if (ltn (len line0) (len line1))
    (print "The first line was shorter.")
    (print "The second line was shorter."))>
  (return "Done.")

Which is clearly not as aesthetically appealing as Haskell, but provides the same usability.

2. Also allows expressing the "applicative style" (i.e., no binding involved) cleanly:

foo = func <$> x <*> y <*> z

becomes

foo = | (func <x <y <z)

Could you do the same I did here, but for your transformation? I.e., specify it, give me a simple example, and show how the snippets above would look on it?

Thanks!

Kesanov commented 7 years ago

Maybe it will be more understandable if I replace | with <=.

Basically it is just a trivial syntactic sugar where

let
<= a <= let -- _ is optional
  b <= foo  _ <= bar -- unless you have multiple assign statements on one line
  in baz b
in a

is rewritten into

(foo b => ( bar _ => (baz b _ => a => a)))

1. It is at least as expressive as do notation.

foo = let
  <= print "Write a line."
  line0 <= getLine
  <= print "Write another line."
  line1 <= getLine
  <= if (ltn (len line0) (len line1))
    (print "The first line was shorter.")
    (print "The second line was shorter.")

  in wrap "Done."

2. Applicative style is not possible.

But do we really need it that much? Yes, sometimes it comes handy, but how often do you actually need it?

3. Also allows nice syntax for unboxing.

let
a <= b <= pair "a" "b"
c = "c"
d <= e <= pair "d" "e"
in con a (con b (con c (con d e)))
VictorTaelin commented 7 years ago

This is really cool though, and easier to grasp, I guess. Not having the applicative style sucks, though, but giving us the pattern matching style for free is very nice. One problem I have is that it would kinda require us to give up on the existing syntax (because it would interfere with stringification, as the stringifier would need to chose one of both - also, "there should be one and only one obvious way to do it"). Hmm...

Edit: this would also make the parser simpler without the lift stuff. This is very Moonish on its spirit, I definitely like it, think it is better than what we have now. This should replace the bang notation. I don't have time to work on it immediately now, but would appreciate a PR. Are the keywords let / in actually necessary?

Kesanov commented 7 years ago

I am wondering whether it would not be better to use an infix pipe operator instead.

foo =
  print "Write a line." | _ =>
  getLine | line0 =>
  print "Write another line." | _ =>
  getLine | line1 =>
  if (ltn (len line0) (len line1))
    (print "The first line was shorter.")
    (print "The second line was shorter.") | _ =>

  wrap "Done."

It looks pretty similar and as a bonus point, there is no ambiguity in deserialization (stringification).


Also IF ELSE syntax looks kinda neat.

( if bar
  ...
| if baz
  ...
| if foo
  ...
| ... )
VictorTaelin commented 7 years ago

But that syntax already works, no? Except we need a lot of parenthesis, of course. But I'm thinking in writing 2.0 version of the parser/stringifier which will kill all those (and more) parenthesis and have the operators... but it doesn't need the |s for that, right?

Kesanov commented 7 years ago

Indeed, the | is not really needed.

VictorTaelin commented 7 years ago

Perhaps the stringifier should just not add indentation on the

(func var =>
body)

pattern, but add on the

(func 
  var => case0
  var => case1)

pattern?

Kesanov commented 7 years ago

That does not cover if though, which should be also indented as:

(if condA
  doA
(if condB
  doB
(if condC
  doC
  doD )))
Kesanov commented 7 years ago

But I'm thinking in writing 2.0 version of the parser/stringifier which will kill all those (and more) parenthesis and have the operators.

Cool. But seriously, currently the error messages from parse errors are horrible. I would focus on that first.

VictorTaelin commented 7 years ago

I think great error messages would be part of a "make a better parser/syntax" project.

VictorTaelin commented 7 years ago

On if, probably a syntax sugar would help a lot:

when:
  condA: x
  condB: y
  condC: z
  default: w

Becomes:

(if condA x
  (if condB y
    (if condC z
      w)))

And

switch value:
  "foo": a
  "bar": b
  "tic": c
  "tac": d
  default: e

Becomes

(if (cmp value "foo") a
(if (cmp value "bar") b
(if (cmp value "tic") c
(if (cmp value "tac") d
  e))))
Kesanov commented 7 years ago

So for the first case the syntactic sugar does not make sense since

(if condA 
  ...
(if condB
  ...
(if condC
  ...
  ...)))

is already very readable.

switch on the other hand is a different entity than if since it can choose branch in O(log(N)) or even O(1) so I would definitely not implement it with if under the hood.


And finally, it is quite easy to detect, when to indent and when not. The rule of thumb is, if you see a repeating pattern like

(val 1
(val 2
(val 3
end)))

then do not indent it. (Such a repeating pattern is usually possible to be replaced with fold. )