1

If a function f: (X => Y) | undefined is possibly undefined, and x: X is defined, then we can use optional-chaining operator ?. to apply f to x:

f?.(x)          // ok, no problem if `f` is undefined

But when f: X => Y is defined and x: X | undefined is possibly undefined, there does not seem to be any syntax to "map" the f over the "optional" x:

f(?.x)          // not valid syntax; Unclear what to do when `x` is undefined

I could try to implement pipeTo to swap the order of f and x, and then make it work with ?. again, as follows:

function opt<X>(x: X | undefined): { pipeTo<Y>(f: (a: X) => Y): Y } | undefined {
    return typeof x === 'undefined' ? undefined : {
        pipeTo<Y>(f: (a: X) => Y): Y {
            return f(x)
        }
    }
}

which I could then use as opt(x)?.pipeTo(f), for example:

function square(n: number): number { return n * n }

for (const x of [42, undefined, 58]) {
  console.log(opt(x)?.pipeTo(square))
}

Is there any less cumbersome standard solution for applying a certainly existing f to a possibly undefined x?


Clarification: "cumbersome" := anything that forces me to write down the subexpression x twice, or to clutter the scope with meaningless helper-variables.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • check x is not nullable ? f(x) : void 0 – kelsny Oct 27 '22 at 18:31
  • Or hell, an if statement if you don't need it to be an expression. – kelsny Oct 27 '22 at 18:31
  • @caTS In both `x is not nullable ? f(x) : ...` and in `if (... x ... ) { ... x ... } ...`, the subexpression `x` appears _twice_, and this is something that I'd really like to avoid. I've added a clarification. I mean, yes, obviously, everything involving `?.` and `??` operators could be written down with variables and `if-else`s, but where's the fun in that? – Andrey Tyukin Oct 27 '22 at 18:39
  • 1
    I don't know how "cumbersome" it is, but essentially you want to map the function over the `Maybe`-like [functor](//en.wikipedia.org/wiki/Functor_(functional_programming)) `type Nullable = T | undefined | null`. That operation is usually called `fmap`. You could call it `fmapNullable` (or something less awkward) and implement it like [this](//tsplay.dev/w2z88w). Not sure if that's actually better or worse than just doing it manually, though. Does that address the question fully? If so I could write up an answer; if not, what am I missing? (Pls mention @jcalz to notify me if you reply) – jcalz Oct 27 '22 at 18:44
  • @jcalz Yes, since `?.` is essentially `Maybe`'s `=>>` / `Option`'s `flatMap`, that's quite unsurprising. I think in your solution, the last `f?.(x)` can actually be replaced by `f(x)` (at least as long you trust your users not to smuggle in a `null` where a `X => Y` was required). If you say that this is currently the most concise solution, then I'd accept it as an authoritative answer. And there are no semi-standard libraries doing something similar? – Andrey Tyukin Oct 27 '22 at 18:52
  • 2
    Oh, right, I had `f?.(x)` in there from when I had originally made it `ap` for applicative functors. Yeah that can be [removed](https://tsplay.dev/mppMBm). I don't really know what to say about "semi-standard libraries" because my expertise outside of the language itself is limited (but asking about libraries in general is rarely in scope for SO so ‍♂️). Anyway I'll write up an answer when I get a chance (I have a backlog right now but I hope to get to it today) – jcalz Oct 27 '22 at 18:57
  • Why not `f && f()`? – Konrad Oct 27 '22 at 19:20
  • @KonradLinkowski because the problem is not with the missing `f`, the problem is with the missing `x`. – Andrey Tyukin Oct 27 '22 at 19:21
  • ... Then it would be `x && f(x)`, but as you have already said, you want to avoid the redundancy of `x` here. – kelsny Oct 27 '22 at 19:26

3 Answers3

2

It's like you're looking at the Nullable<T> type operation (defined as

type Nullable<T> = T | null | undefined

) as a functor and want to perform the functor fmap action on it, to turn a function f of the form (x: X) => Y into another function of the form (x?: Nullable<X>) => Nullable<Y>. I don't think there's any built-in functionality which behaves this way, nor could I speak authoritatively about its presence in third-party libraries, but you could write it yourself easily enough:

const fmapNullable = <X, Y>(f: (x: X) => Y) =>
    (x?: Nullable<X>): Nullable<Y> => x == undefined ? undefined : f(x);

and then use it:

function square(n: number): number { return n * n };

for (const x of [42, undefined, 58]) {
    console.log(fmapNullable(square)(x)?.toFixed(1))
    // "1764.0";
    // undefined;
    // "3364.0"
}

Syntactically I don't think you can get a notation as terse as the optional chaining operator (?.); TypeScript isn't Haskell, after all. You could shorten "fmapNullable", but you'll still be applying a function, so at best you'd have something like $_$(square)(x). Oh well!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 1
    `$_$` - - it actually works with [just two symbols of syntactic overhead](https://stackoverflow.com/a/74228026/2707792). If it were possible to name a method `?`, then one could even make all three `f?.(x)`, `f.?(x)` and `f?.?(x)` work (depending on whether only `f`, only `x`, or both `f` and `x` are missing). Would be quite nice and consistent, actually. – Andrey Tyukin Oct 27 '22 at 20:53
1

This looks like array, so here's an array implementation for you


function nonNullable<T>(v: T | null | undefined): v is T {return v != null}

class OneOrZeroArray<T> extends Array<T> {
    pipe<V>(mapper: (v: T) => V) {
        return this.map(mapper).filter(nonNullable)
    }
    pop(): T | undefined {
        return super.pop();
    }
    static from<T>(item?: T | null | undefined): OneOrZeroArray<T> {
        let a = new OneOrZeroArray<T>();
        if (item != null) a.push(item);
        return a;
    }
}
function opt<T>(v: T | null | undefined) {return OneOrZeroArray.from(v)}

function square(n: number): number { return n * n }

for (const x of [42, undefined, 58]) {
  console.log(
    opt(x).pipe(square).pop()
  )
}
Dimava
  • 7,654
  • 1
  • 9
  • 24
  • Yes, technically, this solves the problem. You've basically reimplemented `java.util.Optional` / `scala.Option`, because an `Option` is just a "list of `X` with at most one element". However, I'm not convinced that writing classes and inheriting from array lists is the most convenient or the most memory-efficient solution (`jcalz`s `fmap` in the comments above achieves the same with much less overhead). But, anyhow, +10 for a principled approach & rediscovering the `Option`-monad. – Andrey Tyukin Oct 27 '22 at 19:35
  • ... and this `pipe` should really be called `map` as well, it's completely analogous to Array's `map`. – Andrey Tyukin Oct 27 '22 at 19:38
  • It occurred to me that this answer is actually more interesting than it might have seem on the first glance. Not only does it reinvent `Option`, it also recognizes it as subfunctor of `List`. Indeed, `[x].filter(i => i).map(f).pop()` behaves as requested in the question. Interesting. – Andrey Tyukin Oct 27 '22 at 21:04
1

Someone, somewhere, probably considered monkey-patching Function:

Function.prototype.$ = function (x) {
  return typeof x === 'undefined' ? undefined : this.apply(null, [x])
}

Seems to work, with exactly two characters of syntactic overhead:

function square(x) { return x * x }
square.$(42)        // 1764
square.$(undefined) // undefined

Please, don't do this. Even if we can, it doesn't mean that we should.

(Also, it's JS, as I currently don't want to try to squeeze it through the TS compiler)

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93