2

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.

9
  • check x is not nullable ? f(x) : void 0 Commented Oct 27, 2022 at 18:31
  • Or hell, an if statement if you don't need it to be an expression. Commented Oct 27, 2022 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-elses, but where's the fun in that? Commented Oct 27, 2022 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 type Nullable<T> = T | undefined | null. That operation is usually called fmap. You could call it fmapNullable (or something less awkward) and implement it like this. 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) Commented Oct 27, 2022 at 18:44
  • 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. 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) Commented Oct 27, 2022 at 18:57

3 Answers 3

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

Sign up to request clarification or add additional context in comments.

1 Comment

$_$ - 😅 - it actually works with just two symbols of syntactic overhead. 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.
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()
  )
}

3 Comments

Yes, technically, this solves the problem. You've basically reimplemented java.util.Optional / scala.Option, because an Option<X> 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 (jcalzs fmap in the comments above achieves the same with much less overhead). But, anyhow, +10 for a principled approach & rediscovering the Option-monad.
... and this pipe<V> should really be called map as well, it's completely analogous to Array's map.
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. 👍
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)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.