HeliosLang/compiler

Extending Helios to support destructuring

vanusquarm opened this issue ยท 20 comments

Helios
Extending Helios language constraint to support destructuring to allow the same operations as seen in the image to generate the same results without errors. This will allow the use of struct types in the same way as primitive types.

In your example you've defined an enum with three variants (newlines don't matter)

Currently you can wrap Buyer:

enum Redeemer {
  Cancel
  Buy {
    buyer: Buyer
  }
}

It's a bit more verbose, but defining methods on Buyer can compensate for that

(please don't get discouraged by me being a bit difficult, I'd like to continue the discussion)

What do you think about the following:

struct Buyer {
  id: PubKeyHash
  amount: Int
}

enum Redeemer {
  Cancel
  Buy {
    buyer: Buyer
  }
}

func main(redeemer: Redeemer) -> Bool {
  redeemer.switch{
    Cancel => true,
    {b: Buyer}: Buy => {...}
  }
}

// and even nested destructuring
func main(redeemer: Redeemer) -> Bool {
  redeemer.switch{
    Cancel => true,
    {{pkh: PubKeyHash, _}}: Buy => {...}
  }
}

// optional typing of intermediate structures
func main(redeemer: Redeemer) -> Bool {
  redeemer.switch{
    Cancel => true,
    {{pkh: PubKeyHash, _}: Buyer}: Buy => {...}
  }
}

The same syntax could be used for function arguments and assignments:

func do_something_w_buyer_pkh({pkh: PubKeyHash, _}: Buyer) -> Bool {
  ...
}

// or

func do_something_w_buyer_pkh(buyer: Buyer) -> Bool {
  {pkh: PubKeyHash, amount: Int}: Buyer = buyer
}

Note: in this proposal the destructuring is positional, not field-name based

  • ๐Ÿ‘ for the recursive destructuring
  • ๐Ÿ‘ as in scala, it should be only positional
  • ๐Ÿ‘ I like the deconstructing in the method signature, one less declaration in the code.
  • I am used to the scala syntax and I would prefer the type to be declared before the attributes Buyer(pkh: PubKeyHash, amount: Int). Not sure if there are language/framework constraints. But I can deffo get used to the type being specified after (that in fairness would follow the convention variable: Type.
  • One request I might have, in scala there is the conditional match: case Buyer(_: PubKeyHash, amount: Int) if amount > 0 =>. It would be awesome if you could implement it as part of this effort.

Type before might work. Would it be better with curly braces though (so it matches literal struct expressions)?

Let's see how it looks when mixing that with the two case syntaxes that already exist:

redeemer.switch{
    c: Cancel => {...},
    Buy{Buyer{pkh: PubKeyHash, _}} => {...},
    (i: Int, _) => {...}
}

// the intermediate struct type is optional
redeemer.switch{
    c: Cancel => {...},
    Buy{{pkh: PubKeyHash, _}} => {...},
    (i: Int, _) => {...}
}

And for function arguments and assignments:

func do_something_w_buyer_pkh(Buyer{pkh: PubKeyHash, _}, other_arg: OtherType) -> Bool {
  ...
}

// or

func do_something_w_buyer_pkh(buyer: Buyer) -> Bool {
  Buyer{pkh: PubKeyHash, amount: Int} = buyer
}

I feel like this works for assignments, but not so much for function arguments

Thanks for the examples, it actually helps a lot seeing them. On a second thought. Maybe type after is better. It's more homogeneous.

Out of curiosity, if intermediate type in this case is optional Buy{{pkh: PubKeyHash, _}} => {...},, why is PubKeyHash required

PubKeyHash is required so the reader knows the type of pkh just be looking at this code. If we allow type-inference here then the reader would have to dig deeper

Note that this issue is only about destructuring, not about special pattern matching syntax

Note that this issue is only about destructuring, not about special pattern matching syntax

ahah ok ok got excited and carried a bit away ...

What do you think about the following:

struct Buyer {
  id: PubKeyHash
  amount: Int
}

enum Redeemer {
  Cancel
  Buy {
    buyer: Buyer
  }
}

func main(redeemer: Redeemer) -> Bool {
  redeemer.switch{
    Cancel => true,
    {b: Buyer}: Buy => {...}
  }
}

// and even nested destructuring
func main(redeemer: Redeemer) -> Bool {
  redeemer.switch{
    Cancel => true,
    {{pkh: PubKeyHash, _}}: Buy => {...}
  }
}

// optional typing of intermediate structures
func main(redeemer: Redeemer) -> Bool {
  redeemer.switch{
    Cancel => true,
    {{pkh: PubKeyHash, _}: Buyer}: Buy => {...}
  }
}

I've just noticed on discord you're working on this, can you confirm this is the design you're following? I would be supportive of this syntax.

I think this design is the most natural extension of the current syntax (because people would rewrite buy: Buy as {buyer: Buyer}: Buy or {{pkh: PubKeyHash, _}}: Buy.

I would like to get more people involved in the discussion though

What do you think about the following:

struct Buyer {
  id: PubKeyHash
  amount: Int
}

enum Redeemer {
  Cancel
  Buy {
    buyer: Buyer
  }
}

func main(redeemer: Redeemer) -> Bool {
  redeemer.switch{
    Cancel => true,
    {b: Buyer}: Buy => {...}
  }
}

// and even nested destructuring
func main(redeemer: Redeemer) -> Bool {
  redeemer.switch{
    Cancel => true,
    {{pkh: PubKeyHash, _}}: Buy => {...}
  }
}

// optional typing of intermediate structures
func main(redeemer: Redeemer) -> Bool {
  redeemer.switch{
    Cancel => true,
    {{pkh: PubKeyHash, _}: Buyer}: Buy => {...}
  }
}

Great work Christian. Love the use-cases, however, will like to discourage the introduction of deep nesting as seen in the last two examples as this introduced a new level of complexities such as the difficulties in tracking missing objects and subsequently the possibilities of getting many undefined errors. Eslint recently removed support for deep nested destructuring with reason of promoting more readable and comprehensive code.

@vanusquarm are there any strongly typed languages that support deeply nested destructuring? (the typing would prevent undefined errors)

You might be right about the readability argument though. However, as enum variants can't yet be assigned type directly (it has to be wrapped), this could be a convenient way to destructure those variants in a single expression

Has been implemented in v0.13.17 of the library.

The syntax differs a bit from what was proposed above, but I think the final implementation is very consistent and conventional

https://www.hyperion-bt.org/helios-book/lang/destructuring.html

A version of the example we've been using above using the latest version:

spending my_validator

struct Buyer {
  id: PubKeyHash
  amount: Int
}

enum Redeemer {
  Cancel
  Buy {
    buyer: Buyer
  }
}

func main(_, redeemer: Redeemer, _) -> Bool {
  redeemer.switch{
    Cancel => true,
    Buy{b} => {doSomething(b)}
  }
}

// and even nested destructuring
func main(_, redeemer: Redeemer, _) -> Bool {
  redeemer.switch{
    Cancel => true,
    Buy{Buyer{pkh, _}} => {doSomething2(pkh)}
  }
}

This is PERFECT