Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do I have to specify parameter names for higher-order function types in TypeScript? [duplicate]

Trying to get my feet wet using TypeScript and I keep running into trouble. An old function resurfaced today and just as an exercise, I was curious if I could convert it to TypeScript. So far it's been a complete pain in the neck.

declare type Ord = number | string;

// type signature for f sucks really bad
// (f: Ord => Ord => boolean) would be really nice, if possible
// but instead I have to give names (_) for the parameters? dumb
const arrayCompare = (f: (_: Ord) => (_: Ord) => boolean) => ([x,...xs]: Ord[]) => ([y,...ys]: Ord[]): boolean => {
  if (x === undefined && y === undefined)
    return true;
  else if (! f (x) (y))
    return false;
  else
    return arrayCompare (f) (xs) (ys);
}

// here the names of the parameters are actually used
const eq = (x: Ord) => (y: Ord) : boolean => x === y;

// well at least it works, I guess ...
console.log(arrayCompare (eq) ([1,2,3]) ([1,2,3]));             // true
console.log(arrayCompare (eq) (['a','b','c']) (['a','b','c'])); // true

So the question is specifically about (see bold)

const arrayCompare = (f: (_: Ord) => (_: Ord) => boolean) => ...

f is expecting a higher-order function of the type

Ord => Ord => boolean

But if I use this type signature

// danger !! unnamed parameters
(f: (Ord) => (Ord) => boolean)

TypeScript will assume Ord as the name of the parameter and the implied type is any

// what TypeScript thinks it means
(f: (Ord: any) => (Ord: any) => boolean)

Of course this is not what I want, but that's what I get anyway. In order to get what I actually want, I have to specify the names of the parameters for the higher-order function

// now it's correct
(f: (_: Ord) => (_: Ord) => boolean)

But c'mon that makes no sense. I only have access to f in this context, not to the parameters that f will bind when I eventually call it...

Question

Why do I have to provide names for higher-order function parameters in TypeScript?

It makes no sense and makes the function signatures long, ugly, harder to write, and harder to read.


UPDATE

"as far as names for parameters, consider a function that takes a callback of -> (number -> number -> number) ->, so based solely on the types your options are: add, subtract, multiply, divide, power, compare of which only one makes sense, now if a callback parameter had a name add: (number -> number -> number) the choice would be obvious" – Aleksey Bykov

I'm happy to be given an opportunity to reply to this. I can name heaps more functions with (number -> number -> number) signature.

  • first, second, mod, min, max
  • bitwise functions &, |, xor, <<, and >>
  • (x, y) => sqrt(sq(x) + sq(y))
  • (x, y) => x + x + y + y + superglobalwhocares
  • and any other function you can dream up

To clear things up, I'm not suggesting the function parameter itself should not be given a name. I'm suggesting that function parameter's parameters should not be given names ...

// this
func = (f: (number => number => number)) => ...

// not this
func = (f: (foo: number) => (bar: number) => number)) => ...

Why? well because f has no knowledge of the parameters of the function that I will be providing.

// for the record, i would never name parameters like this
// but for those that like to be descriptive, there's nothing wrong with these
const add = (addend: number) => (augend: number) => number ...
const sub = (minuend: number) => (subtrahend: number) => number ...
const divide = (dividend: number) => (divisor: number) => number ...
const mult = (multiplicand: number) => (multiplier: number) => number ...

// I could use any of these with my func
func (add ...)
func (sub ...)
func (divide ...)
func (mult ...)

I couldn't provide names for f's parameters in func if I tried ! Because who knows which function I will use? All of them are appropriate.

If I try to put names on them, I pigeonholed the user's imagination of what the function is capable of ...

// maybe the user thinks only a division function can be specified (?)
func = (f: (dividend: number) => (divisor: number) => number) => ...

dividend and divisor are not a good fit here because any of the functions listed above would fit. At best I could do this

// provide generic name for f's parameters
func = (f: (x: number) => (y: number) => number) => ...

But then what's the point? It's not like x and y become bound identifiers. And x and y offer no added description – which I suppose brings me to my point: they're not meant to have a name or description. f has zero knowledge of the way we might use it, but it doesn't matter; as long as it has a (number => number => number) interface, that's all we care about. And that's the most useful information we can provide to the user of our func regarding the f parameter.

"It would be quite confusing for function like:

foo(cb: (number, number) => (number, string) => boolean)

What does it do?" - unional

Same exact reasoning applies here. Besides the fact that (cb: (number, number) => (number, string) => boolean)) is a poorly-designed function (how many useful mixed-type quaternary (4-arity) functions can you name?), it doesn't matter. f can't pretend to know any descriptors about the countless functions I could come up with that use such a signature.

So my question is, why the heck do I have to specify overtly meaningless names for function parameter parameters ?


Exercise

Can you replace _ with meaningful names?

const apply2 = (f: (_: number) => (_: number) => number) => (x: number) => (y: number): number => {
    return f (x) (y)
};

const sqrt = (x: number): number => Math.sqrt(x);
const sq = (x: number): number => x * x;
const add = (addend: number) => (augend: number): number => addend + augend;
const pythag = (side1: number) => (side2: number): number => sqrt(add(sq(side1)) (sq(side2)));

console.log(apply2 (add) (3) (4));    // 7
console.log(apply2 (pythag) (3) (4)); // => 5

If not, can you make a compelling argument why such names must be present in your TypeScript signature?

like image 695
Mulan Avatar asked Feb 19 '17 00:02

Mulan


1 Answers

It's hard to write currying definitions, at least in a way that is readable.
I would do is to extract the signatures outside of the function declaration as much as possible, something like this:

type Ord = string | number;
type ThirdFunction = (objs: Ord[]) => boolean;
type SecondFunction = (objs: Ord[]) => ThirdFunction;
type FirstFunction = (fn: (o: Ord) => (o: Ord) => boolean) => SecondFunction;

const arrayCompare: FirstFunction = f => ([x,...xs]) => ([y,...ys]) => {
    ...
}

(code in playground)

I also removed the declare you had before the Ord type alias, there's no need for it. And you can find better names for the types.
Another thing is that you don't need to specify the boolean here:

const eq = (x: Ord) => (y: Ord) : boolean => x === y;

Can be:

const eq = (x: Ord) => (y: Ord) => x === y;

Or you could express the function using a single type declaration. Readability is fairly decent, all things considered.

type Ord = number | string;

type arrayCompareFunc = (f: (x: Ord) => (y: Ord) => boolean)
                      => (xs: Ord[])
                      => (ys: Ord[])
                      => boolean;

const arrayCompare: arrayCompareFunc = f => ([x,...xs) => ([y,...ys) => {
   ...
};
like image 52
Nitzan Tomer Avatar answered Sep 23 '22 03:09

Nitzan Tomer