Conversation
// With guard: clean and safe
fun processUser(name: String?, age: Int?): User? {
guard let n = name else { return nil }
guard let a = age else { return nil }
return User(name: n, age: a)
}I like it in general but syntax feels strange to me. But probably it is just me edit: also piqued my interest |
|
I like it, but the guard keyword sounds just a bit off... How about syntax like this: if not let a = input.a { return nil;}
// the reasoning is maybe we would want something like this:
if let b = input.b && let c = input.c { //... }
// and then this:
if not let d = input.d && let e = input.e { //... }
|
|
I don't think we should try to come up with new syntax just for the sake of it, especially if there is already an established one we can borrow. The Swift evolution's commonly rejected proposals has a section regarding |
|
I see three equivalent styles here for handling optionals:
fun processUser(name: String?, age: Int?): User? {
if let n = name {
if let a = age {
return User(name: n, age: a)
}
}
return nil
}
fun processUser(name: String?, age: Int?): User? {
if name == nil || age == nil {
return nil
}
return User(
name: name!,
age: age!
)
}
fun processUser(name: String?, age: Int?): User? {
guard let n = name else { return nil }
guard let a = age else { return nil }
return User(name: n, age: a)
}
From my understanding, all three forms are semantically equivalent and compile down to the same short-circuiting logic. Question: |
Because that's the very feature of it: The else branch is the unhappy case and diverges, and must exit, so that the main branch can always assume that the condition held.
No, it does not have any performance/compilation advantages, its primary benefit is readability and safety. |
|
Some additional context: We already have a form of Another related language is Kotlin, which has the "elvis" operator |
jordanschalm
left a comment
There was a problem hiding this comment.
Thanks for writing this up. I think this feature would be helpful for managing optionals in larger functions.
| guard amount > 0.0 else { | ||
| panic("Amount must be positive") | ||
| } | ||
| guard amount <= self.balance else { | ||
| panic("Insufficient balance") | ||
| } |
There was a problem hiding this comment.
These should probably be preconditions even if guard were implemented, so maybe not the best example
There was a problem hiding this comment.
Agreed, given the panics in the else branches, these should be pre-conditions. It's hard to come up with an example that is simple so it brings the point across, yet is also a real-world example.
Would something like this using returns be better?
access(all) fun maybeWithdraw(amount: UFix64): Bool {
guard amount > 0.0 else {
log("failed to withdraw: non-positive amount")
return false
}
guard amount <= self.balance else {
log("failed to withdraw: insufficient funds")
return false
}
self.balance = self.balance - amount
return true
}Maybe with a note that this is just for illustration purposes, and a "production" withdraw function should not be implemented with a boolean return value.
@turbolent so this will be some kind of pre-condition but also will not fail ? better examples can be constructed with failable cast or optional returning functions I guess. guard let vault <- r as? @{FungibleToken.Vault} else {
return nil
}
vault.withdraw(....)guard let flowVaultReference = acc.storage.borrow<&Flowtoken.Vault>(from:/storage/flowTokenVault>() else {
return nil
}I think this is a bit shift in how we write Cadence also. ( in my opinion little conflicting with pre-conditions ) Like functions should return optionals instead of failing. Also there are cases like this: var listed = false
if let marketV3CollectionRef = acct.capabilities.borrow<&TopShotMarketV3.SaleCollection>(/public/topshotSalev3Collection) {
let salePrice = marketV3CollectionRef.getPrice(tokenID: UInt64(5188888))
if salePrice != nil {
listed = true
}
} else if let marketV1CollectionRef = acct.capabilities.borrow<&Market.SaleCollection>(/public/topshotSaleCollection) {
let salePrice = marketV1CollectionRef.getPrice(tokenID: UInt64(5188888))
if salePrice != nil {
listed = true
}
}
if listed {
panic("moment is already listed for sale")
}
which I think guard will not solve. |
Like a pre-condition the condition must succeed, but unlike the pre-condition execution will not necessarily abort execution: the else branch may panic, which would be equivalent, but it may also just return from the function.
That's a good example! 👍
This proposal and feature do not intend to move developers away from pre-conditions or if they should write code that gracefully returns instead of aborting execution. In many cases pre-conditions are a better choice over But in cases where a pre-condition won't be possible, a guard can help, e.g. for unwrapping optionals (and then panicing or returning from the function, to be decided by the developer). The proposal does try to recommend the early return pattern, and proposes a feature that allows expressing it elegantly.
Yeah, in this example a |
|
btw to make it clear, I like this feature (As people know me know well, I am big hater off forced unwrap operator ); now while reading my comments, it seems a bit negative, it is my general personality, always trying to find an edge case. |
SupunS
left a comment
There was a problem hiding this comment.
Excellent proposal! I like the idea of having a way to early exit for "undesired" inputs, and keep the main execution path "clean". 👍
| guard <boolean-expression> else { | ||
| // must exit: return, panic, break, or continue | ||
| } |
There was a problem hiding this comment.
This variant of the syntax seems a bit redundant with the if !<expression> { ... }. My suggestion would be to only keep the below variant (which is very useful!), and defer this variant, so that all developers would stick to the same syntax to achieve similar use-cases.
There was a problem hiding this comment.
This was already kind of addressed above: #356 (comment).
One can think of guard as a slightly stricter "if-not", which performs the same check, but in addition guarantees that the inverse is no longer true, as the else block is required to exit. his is useful for e.g. validating parameters, which are constant. Then again we already have pre-conditions which would be more appropriate in that scenario.
I also find it communicates the "checks" part of the function a bit more, but that is not a strong argument for it.
I'm also happy to leave this out of the proposal.
There was a problem hiding this comment.
I agree that it also seems redundant and I'd be fine leaving it out. If you feel strongly about it though, i'm fine with it because it doesn't really hurt to include it
joshuahannan
left a comment
There was a problem hiding this comment.
Great addition! I'll definitely want to use this in a lot of places
| guard <boolean-expression> else { | ||
| // must exit: return, panic, break, or continue | ||
| } |
There was a problem hiding this comment.
I agree that it also seems redundant and I'd be fine leaving it out. If you feel strongly about it though, i'm fine with it because it doesn't really hurt to include it
| ### Key Design Decisions | ||
|
|
||
| **Mandatory else block exit**: | ||
| The type checker verifies the else block always exits via return, panic, break, or continue. |
There was a problem hiding this comment.
I'm not totally sure how continue works, but am I still allowed to continue out of a regular guard statement that is just in a function and not in a loop or will that not pass type checking?
There was a problem hiding this comment.
continue statements can only be used in loops, this proposal does not change anything about that.
The line includes continue because it satisfies the requirement for exiting from the "main execution path": Once the continue statement is reached, the next iteration starts, and the code in the current iteration is no longer executed.
The implementation PR has some tests, which has good examples for where/how guards can be used: https://github.com/onflow/cadence/pull/4432/changes#diff-a54704e673343eb0960e4f06d4ba5d5de47c7dcd7a53085f9d0a1799de741062
From one of the tests:
// Sum of even numbers from 1 to 10: 2 + 4 + 6 + 8 + 10 = 30
fun test(): Int {
var result = 0
var i = 0
while i < 10 {
i = i + 1
guard i % 2 == 0 else {
continue
}
result = result + i
}
return result
}
Work towards #355