HeliosLang / compiler

Helios is a DSL for writing Cardano smart contracts. This library lets you compile Helios scripts and build Cardano transactions.
https://www.hyperion-bt.org/helios-book
BSD 3-Clause "New" or "Revised" License
142 stars 31 forks source link

[Feature Request] Implement `copy` function #60

Closed nemo83 closed 1 year ago

nemo83 commented 1 year ago

Given the functional nature of helios, it would be amazing a if copy function could be implemented for onchain types. This would make helios less error prone by reducing verbosity.

Example.

We have a combination of lovelace/tokens at a utxo. We know that the value it's legit but we don't care what's in it, neither we want to keep track, and we want to split into two parts:

  1. All the tokens + fixed amount of ada to be sent to an address/script
  2. Whatever is left (fee/costs) to be sent to a wallet.
func correct_value_sent(
    tx: Tx, 
    totalValueLocked: Value,
    vaultPkh: PubKeyHash, 
    walletPkh: PubKeyHash
    ) -> Bool {

    valueSentToVault: Value = totalValueLocked.copy(lovelaces = 2000000)
    costs: Value = totalValueLocked - vaultValue

    walletValue: Value = tx.value_sent_to(walletPkh);
    vaultValue: Value = tx.value_sent_to_datum(vaultPkh, MyDatumDefinition, true);

    (costs >= walletValue) && (valueSentToVault == vaultValue)
  }
christianschmitz commented 1 year ago

Interesting proposal!

Implementing some setters might work for this. The setter then returns the updated value.

Eg.:

valueSentToVault: Value = totalValueLocked.set_lovelace(2000000)

Remember that all values are immutable, so the only thing a setter can do IS return something new.

What do you think?

christianschmitz commented 1 year ago

Not sure if setters should be auto-generated for user types (eg. set_<name-of-field>)

nemo83 commented 1 year ago

Interesting proposal!

Implementing some setters might work for this. The setter then returns the updated value.

Eg.:

valueSentToVault: Value = totalValueLocked.set_lovelace(2000000)

Remember that all values are immutable, so the only thing a setter can do IS return something new.

What do you think?

thanks, I'm only reporting the scala features that I find drastically reduce typing. So I might come up w/ a few more of these as I write more and more helios code.

For the record, I'm totally addicted to the tool.

I don't think I set_ as it recalls mutability, if copy(lovelace = 2000000) isnot possible, I would maybe opt for with_lovelace(2000000) in a builder style approach (that still I don't fully like, but at least is not teh set_.

Is the copy(lovelace= 2000000) hard/impossible to autogenerate? As that would definitely be my favourite options.

christianschmitz commented 1 year ago

We can add copy as special syntax (similar to how switch is special syntax)

But we'd have to be very sure that that's the best syntax possible

Anything that you can think of that can be improved upon wrt. scala?

nemo83 commented 1 year ago

Anything that you can think of that can be improved upon wrt. scala?

Not sure what you mean here, but the copy method is the only scala syntax feature I can think of that could be beneficial to helios. The other thing I use all the time is pattern matching (switch), that helios has already.

Wrt to alternative to the copy I would avoid the set any other thing sounds good to me.

christianschmitz commented 1 year ago

Variations on the syntax are possible:

my_struct.copy(field1 = <expr1>, field2 = <expr1>)

or

my_struct.copy{field1 = <expr1>, field2 = <expr1>}

or

my_struct.copy{field1 => <expr1>, field2 => <expr1>}

or

my_struct.copy{field1: <expr1>, field2: <expr1>}
christianschmitz commented 1 year ago

I think the last one is most consistent with the rest of Helios. @nemo83 What do you think?

nemo83 commented 1 year ago

I think the last one is most consistent with the rest of Helios. @nemo83 What do you think?

I am used to the first example. But I'd be interested in seeing examples of where 4 is already used in helios.

Thanks 🙏

EDIT:

Some addition. I'm not sure the .copy method should be seen as a special syntax. In Functional Programming objects (values) are immutable. Helios structs seem to be what in scala are called case classes and have a .copy method by default. Because of being methods, they have no special syntax. From a user (helios programmer) PoV, forcing a different syntax from a simple method invocation, could communicated that .copy is somehow special, while it's just a method. Also, they would need to rememeber that .copy requires a special syntax.

A simple scala example


case class Person(name: String, age: Int)

val giovanni = Person("Giovanni", 39)

// some time in April

val giovanniIs40 = giovanni.copy(age = 40) // the giovanni object above is still 39 (lucky it!)

I would stick with the syntax I proposed at the beginning of the thread:

newValue: Value = myValue.copy(lovelaces = 2000000)

An option I could consider is to use : instead of = if there is already a convention to assign value to params referenced by name with :.

christianschmitz commented 1 year ago

An issue I see with using scala copy syntax is needing to expand key-based arguments to the rest of the language, otherwise, it feels incomplete.

My reasoning for the braces syntax is that it is like a literal struct, except it does not need all the fields.

christianschmitz commented 1 year ago

Maybe expanding key-based arguments to the rest of the language is not such a bad idea, and also add the capability for optional arguments.

Perhaps we could use the C# syntax ?:

myValue.copy(lovelace: 200000)

Other things to figure out:

func my_func(a: Int, b: Int = 0) -> Int {
   ...
}

func my_func(a: Int, b: Int = a*2) -> Int {
   fn = (c: Int, d: Int = b) -> {...}; ...
}

func my_func(a: Int, b = 0) -> Int {
   ...
}
christianschmitz commented 1 year ago

Actually, now that I think of it: we have to use the colon syntax because the equals syntax is already a part of a valid expression (and function call arguments can be arbitrary expressions)

christianschmitz commented 1 year ago

What should the type signature look like for functions with optional arguments?

Eg.

(a: Int, b? Int) -> Int

Which could be cast into (a: Int) -> Int or (a: Int, b: Int) -> Int automatically, or even (Int) -> Int or (Int, Int) -> Int (arg names are now part of the type)

(I don't think I've ever seen a language do that right)

nemo83 commented 1 year ago

What should the type signature look like for functions with optional arguments?

Eg.

(a: Int, b? Int) -> Int

Which could be cast into (a: Int) -> Int or (a: Int, b: Int) -> Int automatically, or even (Int) -> Int or (Int, Int) -> Int (arg names are now part of the type)

(I don't think I've ever seen a language do that right)

This is a very delicate topic, and in this case I can see now why in a way copy is different.

For me optionality should still be wrapped into the Option[] type.

So the :? syntax would/should be only for copy method as it won't make sense elsewhere.

christianschmitz commented 1 year ago

To be clear: copy might have to be a special builtin function that wouldn't be usable as a value (it would have to be called immediately).

Another version of syntax for functions with optional args could be: (a: Int; b: Int, c: Int) -> Int (so anything after ; is optional)

christianschmitz commented 1 year ago

The semi-colon approach might not be so clean for a function with only optional arguments:

func add(;a: Int, b: Int) -> Int {
   ...
}

An alternative could be:

func add(a: Int, b ? Int = 0) -> Int {
  ...
}

// type-signature of "add":
// (Int, ? Int) -> Int
// (a: Int, b ? Int) -> Int

Or simply reuse = symbol if the conventional syntax is preferred:

func add(a: Int, b: Int = 0) -> Int {
   ...
}

// type-signature of "add":
// (Int, Int =) -> Int 
// (a: Int, b: Int =) -> Int
christianschmitz commented 1 year ago

Another variation in which the type signature differs from the definition:

func add(a: Int, b: Int = 0) -> Int {
   ...
}

// type signature of "add":
// (a: Int, b: ?Int) -> Int
// (Int, ?Int) -> Int

copy could actually be an auto-generated function that can be passed around as a value before calling:

myStruct.copy: (name1: ?Type1, name2: ?Type2, ...) -> MyStruct
christianschmitz commented 1 year ago

I've implemented default values and copy in v0.13.0 of Helios. @nemo83 When you have some time, could you test that it works for your cases?

https://www.hyperion-bt.org/helios-book/lang/functions/optional_arguments.html https://www.hyperion-bt.org/helios-book/lang/automatic-methods.html#copy

nemo83 commented 1 year ago

Huge milestone for Helios.

Less code Less bugs

LFG