Add `filter` and `ifSome(then:)` to Optional

FWIW:

I said it in the past somewhere, and I still believe that porting some Collection methods to Optional would be nice. However I disagree with the ifSome(then:) name. We already have a name for such operation and it’s forEach(_:).

1 Like

We need to stop the forced parallels between Optional and Collection, just because Optional has map and flatMap, doesn’t mean we need to crowbar in every API that seems to do something similar. I’ll defer to my previous statement:

Following this logic, any type that is a functor (implements map) and that is also a monad (is a functor that also implements flatMap) should follow suite. Promise is one such type. Let’s have a Promise.forEach. Compare the syntax:

let p: Promise<File> = downloadFile("www.something.com/file")
p.then { print($0 } // makes sense!

p.forEach { print($0) } // wut

We could make some forced connection between promises and collections, saying that a Promise is a collection of 0 or 1 values: but it just doesn’t serve us any good. We chose then(_:) because it makes more sense than forEach(_:), even if under some contrived conformance to collection, they would do the same thing.

Naming ifNone(then:) opens up the opportunity for also having ifNil(then:). What would the counterpart to forEach(_:) be, in that case?

1 Like

I look at it as a matter of familiarity and recognizability, if you will, of the API. It is not about the Collection-ness of Optionals, but about this specific method, that unwraps the abstraction and applies a closure to every single value that happens to be inside. Promise fits…

I might be brain-damaged beyond repair, but it’s only Result.forEach that seems a little unnatural to me. 🤷‍♂️

Haha fret not, I don’t think you’re brain damaged.

Yes, I did concede that promise “fits” technically speaking:

But I think our APIs should center around what’s intuitively true, over that’s technically true. This is a valid way of looking at it, and I don’t contest that. The issue is that it’s not an obvious observation to make, and would certainly not be obvious to someone learning how to work with optional or promises in the first place. Even for those who do know about this similarity, like you and I, I’m sure that we can agree that getPromise().then(doSomething) is more intuitive.

I’d further like to note, that sometimes you have to work with a collection of promises. Some poorly-designed APIs only work with single cases per request, requiring multiple requests. It’s not a good thing, but it’s a fact of life we need to put up with sometimes. The distinction between the forEach of the collection, and the then of the promise is a useful distinction to make: promises.forEach { $0.then(doSomething) }.

If Promise.forEach isn’t unnatural, why do you find Result.forEach unnatural? Where’s the distinction?

And again, I’d like to pose the question:

I do not understand the ifNil(then:) part, I’m afraid. What’s its signature?

Result contains some payload even in its Left or Error case, which is completely ignored by map and forEach, depending on the implementation of course. That is the unnatural part that I meant.

extension Optional {
	func ifSome<Result>(then closure: (Wrapped) throws -> Result) rethrows -> Optional {
		if let wrapped = self { try _ = closure(wrapped) }
		return self //for chaining to ifNone(then:)
	}
	
	func ifNone<Result>(then closure: () throws -> Result) rethrows {
		if self == nil { try _ = closure() }
	}
}

func doSomething(i: Int) { print(i) }
func doSomethingElse() { print("no value!") }

let i: Int? = 5

i.ifSome(then: doSomething)
 .ifNone(then: doSomethingElse)

This is a contrived example of the usage, but it follows from your earlier point:

The same wrapping a long chain in a for loop is a bit mesy, the same could be said for if. The variable declaration is at the very start of the whole expression, before the chain, yet its usage is in the block at the very end, after the chain:

if let result = Something() // result defined all the way up here
	.doSomething()
	.map { $0.something }
	.filter({ $0.isValid }) { // can't use trailing closure syntax on last line
	use(result) //result used all the way down here
}
else {
	handleNil()
}

compared to:

Something()
    .doSomething()
    .map { $0.something }
    .filter{ $0.isValid } // can use trailing closure here
    .ifSome(then: { result in use(result) }) // result declared close to where its used
    // could even just be: .ifSome(then: use)
    .ifNone(then: handleNil)

What would the forEach equivalent be?

Something()
    .doSomething()
    .map { $0.something }
    .filter{ $0.isValid }
    .forEach(use)
    .ifEmpty(the: handleNil) // Empty? I thought it was called Optional.none or nil!
2 Likes

Oh I see what you’re saying. I personally don’t like this approach where you chain both the “then” and the “else” branches. I’d much rather linearize the chain using something like Optional.orElse(() -> Optional) and then terminate it with a forEach (or whatever the name ends up being).

1 Like

In general I totally agree, but like @moiseev I’d prefer not to chain the alternatives. What I would prefer is

.ifSome(then: {...}, else: {...})

I had considered that, but it doesn’t well if you only care about the nil case. Sure, you could default the then closure to { _ in }, but then it reads as ifSome(else: { ... }), which is not ideal. Even worse, with a trailing closure, it would be ifSome { ... }

I would keep both methods:

ifSome(then:else:) // with the else block defaulting to noop
ifNone(then:)

This is not as nice as it was in Smalltalk where we would have the very regular pair of methods:

ifSome:ifNone:
ifNone:

I’m not sure whether we should try to match the regularity:

ifSome(then:ifNone:) // with the else block defaulting to noop
ifNone(then:)

This isn’t as nice as my original .ifSome, ifThen, because of the look/feel when using traling closures:

.ifSome {
    print("Optional had a value! \($0)"
}
.ifNone {
    print("Optional was nil")
}

vs.

.ifSome(then: {
    print("Optional had a value! \($0)"
},
ifNone:  {
    print("Optional was nil")
})

or

.ifSome(then: {
    print("Optional had a value! \($0)"
}, ifNone:) // valid trailing closure
{
    print("Optional was nil")
}

or

.ifSome(then: {
    print("Optional had a value! \($0)"
}) // normal trialing closure
{
    print("Optional was nil")
}

It is not so bad with a little change in formatting to place both labels on equal footing:

foo.ifSome(
    then: {
        print("Optional had a value! \($0)")
},
    else: {
        print("Optional was nil")
})

or for short actions

foo.ifSome(
    then: { print("Optional had a value! \($0)") },
    else: { print("Optional was nil")})

But it is a matter of taste and your original has the advantage of being able to drop some parentheses by using trailing closures.

Better, but inarguably more cluttered (why waste a full line for then: { and else: {?) and incompatible with trailing closures, which is the nail in the coffin, imo.

What about:

precedencegroup IfSomePrecedence {
    higherThan: NilCoalescingPrecedence
    associativity: right
}
infix operator !! : IfSomePrecedence

extension Optional {
    public func ifSome<R>(_ action: (Wrapped) throws -> R) rethrows -> R? {
        guard let s = self else {
            return nil
        }
        return try action(s)
    }
    public func ifNone(_ action: () throws -> Wrapped) rethrows -> Wrapped {
        return try self ?? action()
    }
    public static func !! <R>(lhs: Optional, rhs: @autoclosure (Wrapped) throws -> R) rethrows -> R? {
        return try lhs.ifSome(rhs)
    }
}

var i: Int?
var r = i.ifSome { _ in false }.ifNone { true }
r // T
r = i !! false ?? true // T

i = 0
r = i.ifSome { _ in true }.ifNone { false }
r // T
r = i !! true ?? false // T

What happened with this pitch?

Can we spin-off at least the non-controversial Optional.filter and drive that through SE proposal?

It may be more productive to file a bug report to request new features.

Forgot about it, and I don't have time to cook up a proposal this week.

Let’s see if this has any legs then…

https://bugs.swift.org/browse/SR-8437

Nope. @jrose said:

API additions always go through the Swift Evolution process.

@AlexanderM would you be fine with splitting out just the filter into its own proposal? Do you have time to do that?

BTW, this optional filter pattern is all over the SE-0222 that’s currently in review.

Finally made a proposal for this: API Additions to Optional (filter, ifSome, ifNone, peek)

Your ifSome is exactly equivalent to the existing Optional.map.

1 Like
Terms of Service

Privacy Policy

Cookie Policy