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

*爱你&永不变心* 提交于 2019-11-29 14:46:13

问题


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?


回答1:


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) => {
   ...
};



回答2:


  1. this is a very impractical way of using functions in JS
  2. TS sucks at generics on function references, so a lot of intuition from Haskell just doesnt work here

back to your question, you have to provide names because TS syntax is such that requires you to do so, rational part is that a name of a parameter conveys additional meaning when the type alone fails to do so




回答3:


When you specify (f: (Ord) => (Ord) => boolean), all TypeScript sees is that your are specifying function with one argument named Ord. You have not specify the type.

EDIT: I can see that it is a limitation in the current TypeScript. A request is filed here: https://github.com/Microsoft/TypeScript/issues/14173

In order to support this syntax, the compiler (and the language service) would need to introduce names themselves.

Consider when the code is being used:

It provides the same syntax as how a function should be defined in TypeScript. i.e. (name: Type) => .... If name is not introduced, it would be very confusing to the user.

On the other hand, if the arguments carry any specific meaning, it worthwhile IMO to provide the argument name, so that user know what to do.

It would be quite confusing for function like:

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

What does it do?




回答4:


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

I don't think they are meaningless. I can think of at least three good reasons why naming the parameters makes sense:

Consistency

This is how you define property types in TypeScript:

class Person {
    public firstName: string;
    public lastName: string;
    public age: number;
}

This is how you specify variable types:

let person: Person;

Parameter types:

function meet(who: Person) {
}

Function and method return types:

function isUnderage(person: Person): boolean {
    return person.age < 18;
}

This is how function type parameters would look without parameter names:

let myFunc: (string, string, number) => boolean;

...or...

function myFuncWithCallback(callback: (string, string, number) => boolean): void {}

...or...

type CallbackType = (string, string, number) => boolean;
let myFunc: CallbackType;
function myFuncWithCallback(callback: CallbackType): void {}

This doesn't quite fit with the other declarations above.

Throughout TypeScript, whenever you use types to signify static typing of a target you go with target: type. That's easy to remember. If you start making rules like: Use the syntax target: type in all cases except when defining parameters of function types, this makes your language less consistent and thus more difficult to learn and to use. It might technically not be necessary, but consistency is a value by itself. JavaScript is full of quirks and TypeScript is inheriting a lot of them. Better not introduce any additional inconsistencies. So this is not an uncommon pattern and that is for good reasons.

this parameters

Without specifying parameter names in function types, specifying this parameters in callback types would become even more messy and inconsistent. Consider this example from the linked page:

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

You would want it like this:

interface UIElement {
    addClickListener(onclick: (this: void, Event) => void): void;
}

So then the rule would be "Use the syntax target: type everywhere, except function types, where it is just the type in the parameters, unless there is a this parameter, then it actually is that syntax, but only in the this parameter." I don't blame the TypeScript designers to rather go with the rule "Use the syntax target: type everywhere."

Development aids

This is what I get when I hover over a function in TypeScript:

How would you like it to read func: (number, Element) => any instead of the descriptive parameter names it has? The type information is pretty useless by itself. And any code automatically generated from this definition would have to give the parameters meaningless names like param1 and param2. That's obviously not optimal.

TypeScript is not the only language naming the paramters of function types:

C# delegates are defined like this:

delegate void EventHandler(object sender, EventArgs e);

Delphi function variables:

type TFunc = function(x: Integer, y: Integer): Integer;


来源:https://stackoverflow.com/questions/42322251/do-i-have-to-specify-parameter-names-for-higher-order-function-types-in-typescri

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!