Why are Maps "write-only" operations failable? #49
Replies: 6 comments 6 replies
-
|
Extra note: I also am not sure if the failure is attached to the |
Beta Was this translation helpful? Give feedback.
-
|
Here is what AndrewS wrote on our internal slack regarding this (someone else asked there): It's a long-standing wart that the compiler thinks the expression as a whole can fail. It should be infallible. What we want to do in the future is to allow |
Beta Was this translation helpful? Give feedback.
-
|
What about splitting the left and right parts of the expression? We already had failure on the right side of map set in the past, but it was removed later after some update, I am unsure why that happened, would love to know more about if possible... That change made us needing to write more workaround syntax that were not needed before. In my mind, the pure map assignments such as |
Beta Was this translation helpful? Give feedback.
-
|
Update on this: @KnightBreaker commented out a important topic with me: Currently in Verse, This can be a conflicting point about the "unfailable map set" idea, however, there is some things to also note: In Verse, nested map set operations mutates the entire outer (MyMap) by making copies of all inner maps with their current and/or updated values as needed. This would only work due to us providing all the keys on that
Another case to consider is with interfaces or classes, since they get passed as references and not copies: Then, for these cases, we could still handle the possible failure for nested classes, and "impossible to fail" cases: Compiler and LSP can already detect if we are making a set operation on a nested map class (Examples 1 and 2), which the map operation is just a "read" where it can fail, or if we are making a set operation on the map itself (Example 3), which is just a "write" operation, without reading the current value, that is unfailable (if no value, it will be created). This "knowledge" can be expanded to the interpreter to be smart about where and how the map access are being used, and with this, giving us more flexibility to not needing to catch false/wrong "impossible-to-fail" errors in the code. |
Beta Was this translation helpful? Give feedback.
-
|
Here are some ideas I have in my in regards of that topic. Possible solutions: To visually explain (B) we will perform a simple pseudo substituation to demonstrate the desired effect. The read or write access to a This gives us the following structure: So far we only changed the syntax from from being (B) proposes that there's an additional non-failable In the following example I'm going to use Swift to fully demonstrate how this could work: // protocol that mimics verse' `<concrete>` effect
protocol Concrete {
init()
}
// `Int` is retroactively made concrete: works because it already provides `init()`.
extension Int: Concrete {}
// Just an error that we will throw when our operation fails.
struct MapError: Error {}
// `Map` type mimics the access of verse' `map` and it's also marked as concrete.
struct Map<Key, Value>: Concrete where Key: Hashable {
// some inner storage
var storage: [Key: Value] = [:]
// Swift cannot express effect on the `subscript.set` accessor, therefore
// we will split that into `set` and `mutate` methods.
// Like in Verse, nothing really is failable, but we marked it anyways.
mutating func `set`(key: Key, value: Value) throws {
storage[key] = value
}
mutating func mutate(key: Key, _ deepMutate: (inout Value) throws -> Void) throws {
if var value = storage[key] {
try deepMutate(&value)
storage[key] = value
} else {
throw MapError()
}
}
}
// Conditional subscript operation as proposed by (B)
extension Map where Value: Concrete {
// conditional set no longer is failable
mutating func conditionalSet(key: Key, value: Value) {
storage[key] = value
}
// conditionalMutate set no longer is failable
// (it does re-throws, only if `deepMutate` throws)
mutating func conditionalMutate<E: Error>(
key: Key,
_ deepMutate: (inout Value) throws(E) -> Void
) throws(E) {
var value = storage[key] ?? Value()
try deepMutate(&value)
storage[key] = value
}
// simpler version of the above:
subscript (key: Key) -> Value {
mutating get {
if let value = storage[key] {
return value
} else {
let value = Value() // init new concrete value
storage[key] = value
return value
}
}
set {
storage[key] = newValue
}
}
}
// Simulating `var Map: [int][int][int]int = map {}`
var map: Map<Int, Map<Int, Map<Int, Int>>> = Map()
print("performing failable example")
// Verse: `set Map[0][1][2] = 3`
do {
try map.mutate(key: 0) { innerMap in
try innerMap.mutate(key: 1) { innerMap in
try innerMap.set(key: 2, value: 3)
}
}
} catch {
print("failure") // executes
}
print("performing non-failable example with concrete")
// Since `Int` and `Map` are both concrete, we can use the
// conditional `*Mutate` and `*Set` methods now without any
// failable context.
map.conditionalMutate(key: 0) { innerMap in
innerMap.conditionalMutate(key: 1) { innerMap in
innerMap.conditionalSet(key: 2, value: 3)
}
}
print(map[0][1][2] == 3) // prints true
// `subscript` in Swift uses `[$]` syntax.
// However we are using the above conditional `subscript` from
// `extension Map where Value: Concrete`.
//
// this would be `set Map(0)(1)(2) = 4` in verse as proposed by (B)
map[0][1][2] = 4
print(map[0][1][2] == 4) // prints trueThis demonstrates that when the compiler knows that |
Beta Was this translation helpful? Give feedback.
-
|
We can even demonstrate how Swift's protocol Concrete {
init()
}
extension Int: Concrete {}
extension Dictionary: Concrete {}
extension Array: Concrete {}
extension Dictionary where Value: Concrete {
subscript (key key: Key) -> Value {
mutating get {
if let value = self[key] {
return value
} else {
let value = Value()
self[key] = value
return value
}
}
set {
self[key] = newValue
}
}
}
// Verse: `var Map: [int][int][]int = map`
var map: [Int: [Int: [Int]]] = [:]
// Verse: `set Map[0][1] = array { 42 }`
map[0]?[1]? = [42] // fails silently in Swift
// Verse: `set Map[0][1] += array { -1 }`
map[0]?[1]? += [-1] // fails silently in Swift
// WITH PROPOSED (B) IDEA:
// Verse: `set Map(0)(1) += array { 1 }`
map[key: 0][key: 1] += [1]
// Verse: `set Map(0)(1) += array { 2 }`
map[key: 0][key: 1] += [2]
// Verse: `for (Element: Map(0)(1))`
for element in map[key: 0][key: 1] {
print(element) // prints only 1 and 2
}As you can see, it does enables very convenient use cases with other operators such as |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Currently in Verse, writting
set MyMap[MyKey] = Valueis a failable operation. But, it is actually impossible to fail, causing confusion on some users, and requiring "workarounds" to ignore that "impossible failure", as shown bellow:A "pure" set/override only map operation (with
=) can't fail, since if the Key does not exist, it will be created or inserted, and, if the key already exists, it will just override it. There is no cases where it can ever fail. Besides this, the LSP/Compiler still marks these operations as failable, requiring us to do workarounds such as:This makes extra annoying to write code due to always needing to create these (unnecessary) "catch" syntax to ignore the impossible failure.
The only cases where a map
setcan fail, is on other operations, such as:+=,-=,/=,*=. This is due to these operations, before actually setting the value, it first try to reads the value at that position, to just then do the increment/operation and setting it back, example:This is correctly failable, because if
MyKeydoes not exist, it can't read from it to increment 10 to store the operation result.The Idea/Proposal:
+=,-=,/=,*=should keep being failable, since it correctly describes their behaviors.=, which is a pure and unfailable set/override, should not be marked as failable, since is impossible to fail.Example signature would be:
Beta Was this translation helpful? Give feedback.
All reactions