I do use Optional.map because it's correct, but I don't actually think it's useful. Do you have an example of it having utility that flatMap does not?
It looks about the same to me. I think Optional.unwrap(_:) is easier to understand, but it gets lost in the details of computing the square in a safe way.
let possibleNumber: Int? = Int("42")
let nonOverflowingSquare = possibleNumber.unwrap { x -> Int? in
let (result, overflowed) = x.multipliedReportingOverflow(by: x)
return overflowed ? nil : result
}
print (nonOverflowingSquare)
However, creating a method safely squaring the an Int would be nice.
func checkedSquare(_ x: Int) -> Int? {
let (result, overflowed) = x.multipliedReportingOverflow(by: x)
return overflowed ? nil : result
}
Then, I think unwrap(_:) looks really good.
let possibleNumber: Int? = Int("42")
let nonOverflowingSquare = possibleNumber.unwrap { checkedSquare($0) }
print (nonOverflowingSquare)
or even better
let possibleNumber: Int? = Int("42")
let nonOverflowingSquare = possibleNumber.unwrap(checkedSquare(_:))
print (nonOverflowingSquare)
I'm interested in being able to quickly understand the code while reading it. Large or complex closures really slow down the reading process.
As a side note, large or complex closures can be a pain to unit test also.
I think it's the transform function, not you, that decides which function to call. I assume you meant the following code:
func increase(_ n: Int) -> Int {
n + 1
}
func increaseOrNil(_ n: Int) -> Int? {
n + 1
}
let x: Int? = 1
print(x.map(increase) ?? "")
print(x.flatMap(increase) ?? "") // This compiles
It compiles only because compiler performs an implicit Optional promotion, otherwise it wouldn't compile.
Swift doesn't support using unspecialized generic type in protocol. If that's possible in future (from what I read in the forum it's not entirely impossible), it would be possible to define protocols like Functor and Monad. The it would be easy to understand that map and flatmap are common methods of these patterns.
Off topic: I was surprised that this compiled at all given that the operands of the ?? have conflicting types (Int? and String). The reason this works is because print expects an argument of type Any, so I'm assuming the typechecker casts the ?? operands to Any? and Any. In contrast, this does not compile:
// Binary operator '??' cannot be applied to operands of type 'Int?' and 'String'
let result = x.map(increase) ?? ""
You'd have to add an explicit type annotation:
let result: Any = x.map(increase) ?? ""
Result is a weird one because it's a container that also has an error in it, sure. I think the intuition you should have is to consider Result<Success, Failure> as if it were something like AbstractResult<Failure>.Concrete<Success>. Then map is the operation that takes (Success) -> NewSuccess and flatMap takes (Success) -> Concrete<NewSuccess>, the same shape as all the rest.
"But what about changing Failure while preserving Success, isn't this symmetric?" Sometimes Result types like this are called biased for this reason, that the default name map means "map success" and there has to be a separate operation mapFailure for the other side, or bimap/mapEither to handle both possibilities in one call.
Okay, now we're back to the original problem: what's the name of the operation where a thrown error is folded into the failure case? I don't think it should just be map. We can't look to Rust for comparison here because Rust only has Result, no throws. We can sort of look to Kotlin, which has exceptions, and see that they call your first method map and your second method mapCatching. So I think the next question (abstractly, Result exists and isn't changing, but if it didn't) would be to see if the Kotlin folks think that's backwards, and would have preferred map and mapThrowing or something.
This thread is evolving into a discussion of theoretical nature. ![]()
The original question was: would unwrap be a better fit to Optional than map?
I am going to ask this again. Why do we even need map on Optional. Does it add any real value at all, unlike in a Sequence?
I can see the beauty and utility of map on a Sequence, but not on an Optional.
PS: @jefferythomas: I am now with you. ![]()
I would just use something like this to alleviate the pain:
func array <T> (from u: Optional <T>) -> [T] {
if let u {
return [u]
}
else {
return []
}
}
What is the purpose of unwrap? Isn't it equivalent to...
extension Optional {
func unwrap() -> Wrapped? {
self
}
}
Sorry, that was a silly error which sneaked through the QA gate. Thank you for spotting it.
What beauty and utility of map on Sequence do you see that does not also apply to Optional?
On sequence, map is an expression-forward way of achieving what could otherwise be done with a for loop statement.
On optional, map is an expression-forward way of achieving what could otherwise be done with an ifβlet statement.
If we dropped map on Sequence, you could simply use more imperative methods and a for loop.
Similarly, with Optional:
return optionalDate.map { dateFn($0) }
?? Date()
// vs.
if let optionalDate {
dateFn(optionalDate)
} else {
Date()
}
It's all just sugar and salt in the end, but if you like map on Sequence, I think the same arguments could apply to Optional.
Thanks for the catch. I intended to use default parameter of string interpolation but forgot the exact syntax. The current code happened to work, so I didn't think much about it.
In my understanding, for types having more than one type parameters map and flatMap are implemented on one type parameter (the others are fixed). Examples in Swift are Dictionary.mapValues, Result.map, Result.mapError, etc. Whether the versions you list should be included probably depends on how useful they are. For example, is it a good practice to mix throwing exception and Result type? I don't have enough experience to comment on it.
And..
When you call Optional.flatMap with a closure, they're almost identical. Since we have implicit optional promotion. That is, a function that expects an Optional<Foo> return value, can be handed a non-optional Foo and the type system will automatically promote it to a Foo?. And when such a function is called with .flatMap it behaves identically to map albeit with first wrapping foo β .some(foo) at return site, and the unwrapping .some(.some(foo)) β .some(foo) in the implementation of flatMap.
Whether or not the compiler may be able to optimize these conversions away, the semantics are identical.
Howver, when calling .flatMap with a function reference, optional promotion does not occur. Swift does not automatically convert an instance of (Foo) -> Bar into (Foo) -> Bar? with an appropriate shim.
I often have an optional String, that I need to convert into a an optional Text:
let subtitle: String?
// ...
let titleView = subtitle.map(Text.init)
let titleView = subtitle.flapMap(Text.init) // ERROR!
In my view, using the name "map" on Optional is a step too far. It works, but only by accident of Optional's shape β and the name map doesn't really suggest "unwrap" anyway.
Optional is an enum, and if you squint it's a collection of one (or zero). But that intuition falls apart on a less convenient enum:
enum E { case foo(Int), bar(Double), baz, qux }
With Array, the identity closure gives you a copy:
items.map { $0 }
For E, what would { $0 } even mean? Different payload types, two cases with no payload, no single thing for $0 to be. Any map here would erase types, pick a case arbitrarily, or really just be a switch in disguise.
Optional's map isn't an application of a general feature β it's a very special case that happens to do its own thing, while being called the same way.
Not true. You just need to use flatJack instead of flapJack. ![]()
![]()
#expect(type(of: (nil as String?).flatMap(Text.init)) == Text?.self) // β
It's not correct thoughβOptional is a functor, while E is not. Thus map applies.
Think you can make E generic like:
enum E<T> { case foo(T), bar(Double), baz, qux }
and then you can do mapping:
extension E {
func map<U>(_ f: (T) -> U) -> E<U> {
switch self {
case .foo(let x):
return .foo(f(x)) // T becomes U
case .bar(let d):
return .bar(d) // unchanged β Double isn't T
case .baz:
return .baz // unchanged β no payload
case .qux:
return .qux // unchanged β no payload
}
}
}
I sympathise to where you are heading with that.. Although for me map defined on an enum should be more like:
enum QuaternaryDigit: Int {
case zero, one, two, three
func map(_ closure: (Int) -> Int) -> Self {
Self(rawValue: closure(rawValue))!
}
}
e.map {
Self(rawValue: ($0 + 1) % 4)!
}
or, equally:
enum QuaternaryDigit {
case zero, one, two, three
func map(_ closure: (Self) -> Self) -> Self {
closure(self)
}
}
var digit = digit.map {
switch $0 {
case .zero: .one
case .one: .two
case .two: .three
case .three: .zero
}
}
which in turn is quite useless, as the same could be achieved without having map at all...
Again, functor map is specifically about the type-level transformation T -> U, just check optional map definition. This wonβt work for simple enum.
Maybe this example shows my line of thinking better. Take this enum:
enum E {
case bool
enum Color { case red, green, blue }
case color: Color
case foo
case bar
}
Its full set of values is:
.bool(false)
.bool(true)
.color(.red)
.color(.green)
.color(.blue)
.foo
.bar
I could transform this set into a list of `E` values
.bool(false) -> .color(.blue)
.bool(true) -> foo
.color(.red) -> foo
.color(.green) -> .bar
.color(.blue) -> color(.blue)
.foo -> bool(true)
.bar -> color(. red)
a list of values of a different type (like `Int`)
.bool(false) -> 390
.bool(true) -> 321
.color(.red) -> 7
.color(.green) -> 3
.color(.blue) -> -4
.foo -> -4
.bar -> 42
or even a heterogeneous list where each element has its own type
.bool(false) -> "hello"
.bool(true) -> 3.1415
.color(.red) -> UIView()
.color(.green) -> UserDefaults.standard
.color(.blue) -> (1, 2, 3)
.foo -> Optional<Int>.none
.bar -> E.Color.green
That transformation is what I'd call "map". Optional's map is something else altogether.
I think that's a backronym. I'm 80% sure that map is called that because its a functor that maps T -> U.
I do not understand this statement, but your link suggests my etymology is correct.
Okay, I'm going to call an end to this argument about the philosophical foundations of reality and truth. I understand the urge to find deep principles to argue from, but there's only so deep you can go with method names before it all gets a little ridiculous.
Jeffery has a very specific proposal that folks can decide whether they like or dislike.