Typescript: Разрешить классам быть параметрическими в других параметрических классах

Созданный на 19 нояб. 2014  ·  140Комментарии  ·  Источник: microsoft/TypeScript

Это предложение о разрешении универсальных шаблонов в качестве параметров типа. В настоящее время можно написать конкретные примеры монад, но для того, чтобы написать интерфейс, который удовлетворяет все монады, я предлагаю написать

interface Monad<T<~>> {
  map<A, B>(f: (a: A) => B): T<A> => T<B>;
  lift<A>(a: A): T<A>;
  join<A>(tta: T<T<A>>): T<A>;
}

Точно так же можно написать конкретные примеры декартовых функторов, но для того, чтобы написать интерфейс, которому удовлетворяют все декартовы функторы, я предлагаю написать

interface Cartesian<T<~>> {
  all<A>(a: Array<T<A>>): T<Array<A>>;
}

Параметры параметрического типа могут принимать любое количество аргументов:

interface Foo<T<~,~>> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

То есть, когда за параметром типа следует тильда и естественная арность, следует разрешить использование параметра типа в качестве универсального типа с заданной арностью в остальной части объявления.

Как и сейчас, при реализации такого интерфейса необходимо указать параметры универсального типа:

class ArrayMonad<A> implements Monad<Array> {
  map<A, B>(f: (a:A) => B): Array<A> => Array<B> {
    return (arr: Array<A>) =>  arr.map(f);
  }
  lift<A>(a: A): Array<A> { return [a]; }
  join<A>(tta: Array<Array<A>>): Array<A> {
    return tta.reduce((prev, cur) => prev.concat(cur));
  }
}

В дополнение к прямому разрешению композиций универсальных типов в аргументах я предлагаю, чтобы определения типов также поддерживали определение универсальных типов таким образом (см. Проблему 308 ):

typedef Maybe<Array<~>> Composite<~> ;
class Foo implements Monad<Composite<~>> { ... }

Чтобы определение типа было допустимым, арности определения и псевдонима должны совпадать.

Suggestion help wanted

Самый полезный комментарий

с образцом мышления HKT можно изменить, привычки сломать, потерянные поколения вернуть к жизни, это было бы самым большим, поскольку дженерики и явные нули и неопределенные значения, он может изменить все

пожалуйста , рассмотреть его в качестве следующей большой функции, остановка слушать людей , которые продолжают просить вас за лучшую лошадь, дать им аф * г феррари

Все 140 Комментарий

Не для того, чтобы делать какие-либо опрометчивые предположения, но я считаю, что вы это неправильно набираете. Для всех типов параметров требуются имена параметров, поэтому вы, вероятно, хотели ввести

map<A, B>(f: (x: A) => B): T<A> => T<B>;

в то время как сейчас map - это функция, которая принимает преобразователь от типа any (где имя вашего параметра - A ) до B .

Для лучших результатов попробуйте использовать флаг --noImplicitAny .

Спасибо, поправили.

Я преобразовал свой комментарий в предложение.

: +1: тип более высокого порядка был бы большим бонусом для конструкции функционального программирования, однако до этого я бы предпочел иметь правильную поддержку функций высшего порядка и универсального: p

Квази-одобрено.

Нам очень нравится эта идея, но нам нужна рабочая реализация, чтобы попытаться понять все последствия и возможные крайние случаи. Следующим шагом было бы действительно полезно иметь образец PR, который, по крайней мере, касается 80% случаев использования этого.

Что люди думают о синтаксисе тильды? Альтернативой T~2 будет что-то вроде

interface Foo<T<~,~>> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

что позволяет напрямую комбинировать универсальные типы вместо использования псевдонимов типов:

interface Foo<T<~,~,~>, U<~>, V<~, ~>> {
  bar<A, B, C, D>(a: A, f: (b: B) => C, d: D): T<U<A>, V<B, C>, D>;
}

Странно иметь явную арность, потому что мы больше нигде этого не делаем, поэтому

interface Foo<T<~,~>> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

немного яснее, я знаю, что другие языки используют * в аналогичных контекстах вместо ~ :

interface Foo<T<*,*>> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

Если довести эту точку зрения до крайности, вы можете получить:

interface Foo<T: (*,*) => *> {
  bar<A, B>(f: (a: A) => B): T<A, B>;
}

Я думаю, что T<~,~> тоже понятнее, чем T~2 . Я изменю предложение выше. Меня не волнует, используем ли мы ~ или * ; он просто не может быть идентификатором JS, поэтому мы не можем использовать, скажем, _ . Я не понимаю, какие преимущества дает нотация => ; все дженерики принимают некоторые типы ввода и возвращают единственный тип вывода.

Облегченный синтаксис полностью отказался бы от арности дженериков; синтаксический анализатор определит это с первого использования и выдаст ошибку, если все остальное не согласуется с ним.

Я буду рад начать работу по реализации этой функции. Какой форум рекомендуется для приставания к разработчикам по поводу деталей реализации транспилятора?

Вы можете зарегистрировать много новых проблем для более крупных вопросов с более задействованными примерами кода или создать длительную проблему, задав серию вопросов по ходу дела. Кроме того, вы можете присоединиться к чату здесь https://gitter.im/Microsoft/TypeScript, и мы сможем поговорить там.

@metaweta есть новости? Если вам нужна помощь / обсуждение, я буду рад провести мозговой штурм по этому вопросу. Мне очень нужна эта функция.

Нет, дела на работе занимали то свободное время, которое у меня было, чтобы поработать.

bump: есть ли шанс увидеть эту функцию когда-либо в расчет?

https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288 - все еще текущее состояние. Я не вижу здесь ничего, что заставило бы нас изменить приоритет функции.

Мне кажется, что это полезно в гораздо большем количестве ситуаций, чем просто импорт абстракций теории категорий. Например, было бы полезно иметь возможность писать фабрики модулей, которые принимают реализацию (конструктор) Promise в качестве аргумента, например, базу данных с подключаемой реализацией обещания:

interface Database<P<~> extends PromiseLike<~>> {   
    query<T>(s:string, args:any[]): P<T> 
}

: +1:

с образцом мышления HKT можно изменить, привычки сломать, потерянные поколения вернуть к жизни, это было бы самым большим, поскольку дженерики и явные нули и неопределенные значения, он может изменить все

пожалуйста , рассмотреть его в качестве следующей большой функции, остановка слушать людей , которые продолжают просить вас за лучшую лошадь, дать им аф * г феррари

Да, столкнулся с этим в первые 15 минут после попытки добавить типы в существующую кодовую базу JS. Я не перехожу на TS, пока не увижу.

Собственно, чем могу помочь?

Интересно, какое отношение это имеет к # 7848? Они очень похожи, хотя касаются других аспектов более высокого порядка.

@ boris-marinov В ответе Райана Кавано говорится, что вы можете:

Следующим шагом было бы действительно полезно иметь образец PR, который, по крайней мере, касается 80% случаев использования этого.

Теперь у меня есть время реализовать такой простой PR. Надеюсь получить подсказки от основных разработчиков, но пока нет вопросов - все выглядит хорошо и понятно. Отслежу прогресс здесь.

@Artazor Хочешь взглянуть и на Cracking # 7848? Это позаботится о другой стороне этой проблемы, связанной с дженериками, и ИМХО, это было бы неполным без него (общие параметры действительно упростили бы много кода уровня типа).

Я считаю это предложение абсолютно замечательным. Наличие более высокодородных типов в TypeScript подняло бы его на новый уровень, на котором мы могли бы описывать более мощные абстракции, чем то, что возможно в настоящее время.

Однако разве что-то не так с примерами, приведенными в OP? A в строке

class ArrayMonad<A> implements Monad<Array> {

не используется ни в одном из методов, поскольку все они имеют свой собственный общий A .

Кроме того, если реализовать функтор с map в качестве метода, использующего this как он будет выглядеть? Может, вот так?

interface Functor<T, A> {
  map<B>(f: (a: A) => B): T<A> => T<B>;
}

class Maybe<A> implements Functor<Maybe, A> {
  ...
}

@paldepind Отъезд # 7848. Это обсуждение касается этого конкретного варианта использования, хотя, IMHO, то и это действительно нужно объединить в один PR.

Когда эта штука приземлится? Это кажется важным.

Также будет возможно такое:

interface SomeX<X, T> {
   ...// some complex definition
  some: X<T>
}

interface SomeA<T> extends SomeX<A, T> {
}

?

@whitecolor Я думаю, что сейчас есть более крупная рыба, которую нужно жарить, что заслуживает более высокого приоритета:

  1. TypeScript 2.0 RC был выпущен чуть менее двух недель назад. Само по себе это займет много времени.
  2. bind , call и apply , собственные функции JS, не типизированы. На самом деле это зависит от предложения вариативных дженериков . Object.assign также нуждается в аналогичном исправлении, но сами по себе вариативные дженерики не решат этого.
  3. Такие функции, как _.pluck Lodash, методы get и set моделей Backbone и т. Д. В настоящее время не типизированы , и исправление этого в основном делает Backbone более безопасным для использования с TypeScript. Это также может иметь последствия для React в будущем .

Не то чтобы мне не нужна эта функция (я бы _любил_ за такую ​​функцию), я просто не думаю, что она скоро появится.

@isiahmeadows
Спасибо за объяснение. Да, третий пункт в списке очень важен, тоже ждем https://github.com/Microsoft/TypeScript/issues/1295 .

Но я надеюсь, что текущая проблема может быть как-то в 2.1dev.

Я согласен. Надеюсь, он сможет это сделать.

(Полиморфизм 2-го ранга, которого требует эта проблема, также необходим для
Пользователи Fantasy Land должны правильно вводить различные ADT в этой спецификации.
Ramda - хороший пример библиотеки, которой это очень нужно.)

6 сентября 2016 г., 11:00 Alex [email protected] написал:

@isiahmeadows https://github.com/isiahmeadows
Спасибо за объяснение. Да, третий пункт в списке очень важен,
ждем # 1295 https://github.com/Microsoft/TypeScript/issues/1295
тоже.

Но я надеюсь, что текущая проблема может быть как-то в 2.1dev.

-
Вы получаете это, потому что вас упомянули.

Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -244978475,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AERrBMvxBALBe0aaLOp03vEvEyokvxpyks5qnX_8gaJpZM4C99VY
.

Похоже, эта функция очень поможет нам в определении форм реакции. Например, у вас есть структура:

interface Model {
  field1: string,
  field2: number,
  field3?: Model
}

У меня есть обработчик, который определяется как:

interface Handler<T> {
  readonly value: T;
  onChange: (newValue: T) => void;
}

этот обработчик передается как свойство компонентам React. Также у меня есть функция, которая принимает структуру и возвращает ту же структуру, но с обработчиками вместо значений:

function makeForm(value: Model): {
  field1: Handler<string>,
  field2: Handler<number>,
  field3: Handler<Model>,
}

На данный момент я не могу правильно ввести эту функцию, потому что TS не может создавать тип на основе структуры другого типа.

Корова Я мог бы ввести makeForm с помощью HKT?

Хм, интересно.

Может быть, возможно что-то подобное:

//Just a container
interface Id <A> {
  value: A
}

interface Model <T> {
  field1: T<string>,
  field2: T<number>,
  field3?: T<Model>
}

makeForm (Model<Id>): Model<Handler>

@ boris-marinov Самым интересным моментом является эта строка:

interface Model<T> {
  //...
  field3?: T<Model> // <- Model itself is generic.
                    // Normally typescript will error here, requiring generic type parameter.
}

Возможно, стоит упомянуть, что HKT мог быть ответом на так называемые частичные типы (https://github.com/Microsoft/TypeScript/issues/4889#issuecomment-247721155):

type MyDataProto<K<~>> = {
    one: K<number>;
    another: K<string>;
    yetAnother: K<boolean>;
}
type Identical<a> = a;
type Optional<a> = a?; // or should i say: a | undefined;
type GettableAndSettable<a> = { get(): a; set(value: a): void }

type MyData = MyDataProto<Identical>; // the basic type itself
type MyDataPartial = MyDataProto<Optional>; // "partial" type or whatever you call it
type MyDataProxy = MyDataProto<GettableAndSettable>; // a proxy type over MyData
// ... etc

Не совсем. {x: number?} нельзя присвоить {x?: number} , потому что один
гарантированно существует, а другой - нет.

Вт, 11.10.2016, 09:16 Алексей Быков [email protected] написал:

стоит упомянуть, что HKT мог быть ответом на так называемые
частичные типы (# 4889 (комментарий)
https://github.com/Microsoft/TypeScript/issues/4889#issuecomment-247721155
):

тип MyDataProto один: K;
другой: K;
еще один: K;
} type Identical = a; type Optional = a ?;
= {get (): a;

;
;
;

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -252913109,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AERrBNFYFfiW01MT99xv7UE2skQ3qiPMks5qy4wRgaJpZM4C99VY
.

@isiahmeadows, вы правы, на данный момент нет способа / синтаксиса, чтобы сделать свойство действительно необязательным, основываясь исключительно на его типе, и это позор

Еще один: было бы хорошо, если бы собственность можно было сделать readonly . Кажется, требуется какая-то функция макросов.

Просто выбросьте это там ... Я предпочитаю синтаксис * синтаксису ~ . Что-то в ~ просто кажется таким далеким с точки зрения раскладки клавиатуры. Кроме того, я не уверен, почему, но я думаю, что * кажется немного более читаемым / различимым со всеми угловыми скобками, которые есть в смеси. Не говоря уже о том, что люди, знакомые с другими языками, такими как Haskell, могут сразу связать синтаксис с HKT. Кажется более естественным.

Придется согласиться с синтаксисом * . Во-первых, он более различим,
во-вторых, он лучше представляет тип «работает любой тип».


Исайя Медоуз
[email protected]

В воскресенье, 6 ноября 2016 г., в 00:10, Landon Poch [email protected]
написал:

Просто выбросьте это там ... Я предпочитаю синтаксис * синтаксису ~.
Что-то в ~ просто кажется таким далеким от раскладки клавиатуры
перспектива. Кроме того, я не уверен, почему, но я думаю, что * кажется немного больше
читаемые / различимые со всеми угловыми скобками, которые есть в смеси.
Не говоря уже о том, что люди, знакомые с другими языками, такими как Haskell, могут
сразу свяжите синтаксис с HKT. Кажется более естественным.

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -258659277,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AERrBHQ4SYeIiptB8lhxEAJGOYaxwCkiks5q7VMvgaJpZM4C99VY
.

Веха: community ? Каково текущее состояние этой проблемы / функции?

@whitecolor статус DIY (сделай сам)

Проблема имеет ярлык Accepting PRs . это означает, что запросы на включение для реализации этой функции приветствуются. См. Https://github.com/Microsoft/TypeScript/wiki/FAQ#what -do-the-labels-on-these-issues-mean для получения дополнительных сведений.

Также см. Https://github.com/Microsoft/TypeScript/issues/1213#issuecomment -96854288

Хорошо, я вижу ярлыки, просто сомневаюсь, что команда, не являющаяся TS, способна на это.

Теперь у меня есть время реализовать такой простой PR. Надеюсь получить подсказки от основных разработчиков, но пока нет вопросов - все выглядит хорошо и понятно. Отслежу прогресс здесь.

@Artazor Тебе с этим повезло?

@raveclassic - это оказалось сложнее, чем казалось, но я все же надеюсь двигаться дальше. Синтаксически это очевидно, но правила / этапы проверки типов мне не так понятны, как хотелось бы -)

Попробуем оживить мою активность -)

Просто отслеживание прогресса и пути развития идеи. Я рассмотрел три варианта реализации этой функции.

Я планировал обогатить TypeParameterDeclaration необязательным свойством higherShape

    export interface TypeParameterDeclaration extends Declaration {
        kind: SyntaxKind.TypeParameter;
        name: Identifier;
        higherShape?: HigherShape // For Higher-Kinded Types <--- this one 
        constraint?: TypeNode;

        // For error recovery purposes.
        expression?: Expression;
    }

и рассмотрели три варианта реализации HigherShape

1. Простая арность для области

type HigherShape = number

это соответствует следующему использованию:

class Demo<Wrap<*>, WrapTwo<*,*>> {   // 1 and 2
    str: Wrap<string>;
    num: Wrap<number>;
    both: WrapTwo<number, string>;
}

в этом простейшем случае, похоже, будет достаточно типа number . Тем не менее, мы должны иметь возможность определять фактическую более высокую форму для каждого данного типа, чтобы быть уверенными, что мы можем использовать ее в качестве аргумента типа для конкретных требований к форме. И здесь мы сталкиваемся с проблемой: более высокая форма самого класса Demo не выражается в виде числа. Если да, то он должен быть представлен как 2 - поскольку он имеет два параметра типа,
и можно было бы написать

var x: Demo<Array, Demo>

а затем борьба с проблемой отложенной проверки типов с помощью свойства .both . Таким образом, типа number недостаточно (я считаю);

фактически тип Demo имеет следующую форму высокого порядка:

(* => *, (*,*) => *) => *

2. Полностью структурированный домен и ко-домен

Затем я исследовал противоположное, наиболее полное представление высших форм, которое позволило бы представить такие формы, как вышеупомянутая, и даже хуже:

(* => (*,*)) => ((*,*) => *)

Структура данных для этого проста, но она плохо взаимодействует с системой типов TypeScript. Если бы мы разрешили такие типы более высокого порядка, мы никогда не узнаем, означает ли * тип земли, который может использоваться для ввода значений. Кроме того, мне даже не удалось найти подходящий синтаксис для выражения столь чудовищных ограничений высшего порядка.

3. Структурированный домен / неявный простой ко-домен

Основная идея - выражение типа (даже с фактическими аргументами типа) всегда приводит к основному типу, который можно использовать для ввода переменной. С другой стороны, каждый параметр типа может иметь свои собственные подробные параметры типа в том же формате, который используется в других местах.

Это было мое окончательное решение, которое я постараюсь отстаивать.

type HigherShape = NodeArray<TypeParameterDeclaration>;

пример:

class A {x: number}
class A2 extends A { y: number }
class Z<T> { z: T; }

class SomeClass<T1<M extends A> extends Z<M>, T2<*,*<*>>, T3<* extends string>> {
        var a: T1<A2>; // checked early
        var b: T2<string, T1>; // second argument of T2 should be generic with one type parameter  
        var c: T3<"A"|"B">; // not very clever but it is checked
        // ...
        test() {
             this.a.z.y = 123 // OK
             // nothing meaningful can be done with this.b and this.c
        }
}

Здесь я хочу отметить, что M является локальным для T1<M extends A> extends Z<M> и существует в более глубокой области видимости, чем T1. Таким образом, M недоступен в теле SomeClass .
А * означает просто новый идентификатор (анонимный тип), который никогда ни с чем не конфликтует (и может быть реализован на более позднем этапе).


Таким образом, окончательная подпись TypeParameterDeclaration

    export interface TypeParameterDeclaration extends Declaration {
        kind: SyntaxKind.TypeParameter;
        name: Identifier;
        typeParameters?: NodeArray<TypeParameterDeclaration> // !!! 
        constraint?: TypeNode;

        // For error recovery purposes.
        expression?: Expression;
    }

Хочу услышать любое мнение @DanielRosenwasser , @ aleksey-bykov, @isiahmeadows и других -)

Звучит нормально, но я очень мало знаю о внутренней структуре кодовой базы TypeScript.

Хочу добавить свой голос к просящему хора и подбодрить вас, Артазор! :)

Эта функция была бы полезна мне в моей реализации по обеспечению типобезопасности Redux.

@michaeltontchev Какие проблемы у вас возникают при обеспечении типизации Redux?

Если вам интересно, я недавно опубликовал https://github.com/bcherny/tdux и https://github.com/bcherny/typed-rx-emitter , которые основаны на идеях Redux и EventEmitter.

Теперь, похоже, нужно переустановить ветку

@bcherny - спасибо за ссылки, проверю!

Я искал, как сделать combReducers типобезопасным, убедившись, что у него есть редуктор нужного типа для каждого свойства состояния (без дополнительных функций). В данном конкретном случае мне удалось сделать это без вложенных дженериков, но вложенное решение было бы лучше. Имею следующее:

import { combineReducers, Reducer } from 'redux';

interface IState {
    // my global state definition
}

type StatePropertyNameAndTypeAwareReducer\<S> = {
    [P in keyof S]: Reducer<S[P]>;
};

let statePropertyToReducerMap : StatePropertyNameAndTypeAwareReducer<IState> = {
    navBarSelection: navBarReducer,
};

let combinedReducers = combineReducers<IState>(statePropertyToReducerMap);

По сути, тип, который я представил выше, гарантирует, что сопоставления редуктора, которые вы передаете в combReducers, охватывают все свойства вашего состояния и имеют правильные возвращаемые типы. Мне не удалось найти такого решения при поиске в Интернете - мне кажется, что это невозможно без функции keyof, которая появилась всего два месяца назад :)

Я ожидаю, что функция keyof пригодится и для ImmutableJs, чтобы сделать сеттеры и геттеры типобезопасными, хотя вам все равно могут понадобиться дополнительные инструменты для этого.

Изменить: чтобы уточнить, вложенные универсальные шаблоны позволили бы мне не жестко кодировать тип Reducer в типе StatePropertyNameAndTypeAwareReducer, а вместо этого передавать его как универсальный, но он должен быть вложенным универсальным, что невозможно в момент.

Edit2: создал проблему для Redux здесь: https://github.com/reactjs/redux/issues/2238 Похоже, они не очень много делают с TypeScript, поэтому они ищут людей TypeScript, которые знают Redux, чтобы взвесить.

Как дела?

Может быть, наивный вопрос, но почему ~ или * вместо обычного универсального параметра? Это означает, что он не используется? Т.е. почему бы и нет:

type Functor<A<T>> = {
  map(f: (value: T) => U): A<U>
}

Или же:

kind Functor<A<T>> = {
  map(f: (value: T) => U): A<U>
}

Или даже:

abstract type Functor<A<T>> = {
  map(f: (value: T) => U): A<U>
}

@bcherny Я считаю, что это вызывает двусмысленность в синтаксисе, поскольку Functor<A<T>> раньше означало " A из T ", где T - это некоторый тип в локальном объем. Маловероятно, но этот синтаксис также может оказаться критическим изменением для некоторых кодовых баз по той же причине.

@masaeedu Понятно . Новый синтаксис означает «связывать T лениво», а не «связывать T строго в текущей области».

Тем не менее, я думаю, что предложение T: * => * имеет здесь наибольший смысл, поскольку для него существует "предшествующий уровень техники".

В Haskell оператор -> на самом деле является параметризованным типом (возможно, его проще визуализировать Func<TArg, TRet> ). Конструктор типа -> принимает два произвольных типа T и U и создает тип конструктора значения (т.е. функции), который отображает значения типа T на значения типа U .

Интересно то, что это еще и добрый конструктор! Конструктор вида -> принимает два произвольных вида T* и U* (звездочка только для визуального различия) и создает конструктор типа, который отображает типы типа T* к видам вида U* .

На этом этапе вы можете заметить закономерность. Синтаксис и семантика, используемые для определения и ссылки на типы, просто повторно используются для определения и ссылки на типы. Фактически, он даже не используется повторно, он просто неявно определяет вещи одновременно в двух разных вселенных. (тот факт, что он изоморфен, на самом деле означает, что он способен определять вещи на бесконечных уровнях, значения -> типы -> виды -> сортировки -> ..., за исключением неудачного * , но это тема для другого времени)

Фактически, этот шаблон имеет такой смысл, что некоторые люди реализовали широко используемое расширение GHCi, которое обобщает его на все конструкторы типов, а не только на -> . Расширение называется «типы данных», и именно поэтому Haskell предлагает гетерогенные списки ( [] - это и тип списков значений, и тип списков типов), гетерогенные кортежи, «умная длина» "векторы, а также многие другие функции.

Возможно, мы пока не хотим доходить до DataKinds , поэтому мы просто будем придерживаться конструкторов вида * и -> , но если мы будем следовать синтаксису, предложенному Дэниелом , или, в более общем смысле, сделав определения вида изоморфными определениям типов, мы открываем себя, чтобы воспользоваться преимуществами будущего развития в этой области.

Следуя моему предыдущему бессвязному посту, я бы порекомендовал использовать any вместо * ; это представляет как тип каждого значения, так и вид каждого типа. Если синтаксис кажется запутанным, мы могли бы взять страницу из книги Haskell и использовать префикс ' для устранения неоднозначности типов и типов.

Тогда пример OP будет записан так:

interface Monad<(T: 'any => 'any)> {
    // ...
}

Nitpick: Я считаю, что any сбивает с толку в том смысле, что он делает две разные вещи.
Это супертип всех остальных, как никогда не является подтипом всех остальных, поэтому, если функция запрашивает параметр any , вы можете вставить что угодно. Все идет нормально.
Забавно бывает, когда функция запрашивает что-то конкретное, а вы предоставляете any . Этот тип проверяет, в то время как любой другой тип, более широкий, чем запрошенный, вместо этого выдает ошибку.
Но да, что угодно.

С другой стороны, ' может сбить с толку, поскольку он также используется в строковых литералах.

@Artazor Есть новости об этом? В прошлый раз, когда вы упомянули, вам нужно перенастроить общие параметры по умолчанию. И мне кажется, что вы единственный, кто достаточно близок к работающему POC.

Также стоит подумать о том, как это взаимодействует с подтипами. Использование * самого по себе недостаточно; в языках, которые используют специальный полиморфизм вместо ограниченного полиморфизма, у вас есть виды ограничений для ограничения допустимых аргументов типа. Например, вид Monad T на самом деле Constraint , а не * .

В TypeScript вместо этого мы используем структурные подтипы, поэтому наши виды должны отражать отношения подтипов между типами. Документ Scala по этому вопросу может дать некоторые хорошие идеи о том, как представить отношения дисперсии и подтипов в системе типов : «

Есть ли в этом прогресс?

Альтернативный подход @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe

Проблема с подходом, принятым fp-ts, заключается в том, что он заставляет вас повторно реализовать проверенные иначе библиотеки. На мой взгляд, идея машинописного текста состоит в том, чтобы иметь возможность правильно печатать то, что в настоящее время считается передовым опытом в JavaScript, а не заставлять вас заново реализовывать это способом ts.

Здесь есть много примеров, которые показывают, что HKT необходим для правильного описания контрактов, которые мы в настоящее время используем в js-библиотеках, будь то фэнтезийная страна, рамда или формы реакции.

Было бы очень приятно увидеть это вживую :)

~ Есть ли кто-нибудь желающий / способный работать на этом платном? Не стесняйтесь обращаться ко мне для обсуждения. Или кто-нибудь может научить кого-то, кого мы можем найти для работы над этим, пожалуйста, дайте мне знать. ~ [EDIT: Я [вероятно решил] (https://github.com/keean/zenscript/issues/35#issuecomment -357567767), чтобы отказаться от этой экосистемы, и мой последующий комментарий в этой теме заставил меня понять, что это, вероятно, будет масштабным мероприятием]

Альтернативный подход @gcanti https://medium.com/@gcanti/higher -kinded-types-in-typescript-static-and-fantasy-land-d41c361d0dbe

Я не стал полностью разбираться в этом, потому что я заметил, что результирующий map прежнему явно указывает тип контейнера Option и, следовательно, не является полностью универсальным в том смысле, в котором типы более высокого порядка (HKT) может обеспечить:

function map<A, B>(f: (a: A) => B, fa: HKTOption<A>): Option<B> {
  return (fa as Option<A>).map(f)
}

Как отметил @spion 26 августа 2016 г. , HKT необходимы для создания универсальной любой функции, которая нуждается в фабрике и в которой тип параметризованного типа контейнера должен быть универсальным. Мы исследовали это при обсуждении дизайна языков программирования.

PS Если вам интересно, эта функция существенно влияет на мой (включая @keean ) анализ ландшафта языков программирования .

@ shelby3 FWIW Option mapOption.ts ) не является универсальным, поскольку представляет экземпляр, а Functor mapFunctor.ts ) является универсальным, поскольку представляет класс типа . Затем вы можете определить общую функцию lift которая может работать с любым экземпляром функтора.

Было бы очень приятно увидеть это вживую :)

Полностью согласен :)

@ shelby3 : чтобы объединить такую ​​функцию, лучше всего получить
им сделать это в дорожной карте TS; У меня было несколько PR, которые в первую очередь получили
обратная связь / слияние либо при небольших исправлениях, либо если они уже искали
в них. Я не хочу быть отрицательным, но это нужно учитывать, если ты
собираюсь вложить в это ресурсы.

8 января 2018 г. в 16:05 "shelby3" [email protected] написал:

Есть ли кто-нибудь желающий / способный работать на этом платном? Не стесняйся связаться
мне [email protected] для обсуждения. Или любой может кого-то тренировать
мы могли бы поработать над этим, пожалуйста, дайте мне знать.

Альтернативный подход @gcanti https://github.com/gcanti
https://medium.com/@gcanti/higher -kinded-types-in-
Машинопись-статика-и-фантазия-земля-d41c361d0dbe

Я не стал полностью разбираться в этом, потому что я наблюдаю получившуюся карту
по-прежнему явно указывает тип контейнера Option и, следовательно, не полностью
универсальным способом, который могут предоставить более высокодородные типы (HKT):

карта функций (f: (a: A) => B, fa: HKTOption ): Option { return (fa как опция ) .map (f) }

HKT необходимы для создания общей любой функции, требующей фабрики и
в котором тип параметризованного типа контейнера должен быть универсальным. Мы имеем
изучили этот https://github.com/keean/zenscript/issues/10 в нашем
дискуссии о дизайне языков программирования.

PS Если вам интересно, эта функция существенно влияет на мои
(включая @keean https://github.com/keean ) анализ
среда языков программирования
https://github.com/keean/zenscript/issues/35#issuecomment-355850515 . я
понимаете, что наши взгляды не полностью соответствуют приоритетам Typescript
с основной целью быть надмножеством Javascript / ECMAScript и поддерживать
эта экосистема.

-
Вы получаете это, потому что подписаны на эту ветку.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/Microsoft/TypeScript/issues/1213#issuecomment-355990644 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AC6uxYOZ0a8G86rUjxvDaO5qIWiq55-Fks5tIi7GgaJpZM4C99VY
.

@gcaniti извиняюсь за шум и благодарим за дополнительные объяснения. Я должен был изучить больше, прежде чем комментировать. Конечно, это моя ошибка в концептуализации, потому что (я уже знаю) функтор требует реализации экземпляра.

Afaics, ваш умный «хак» позволяет ссылаться на фабрику в общем (например, lift ), но требует дополнительного шаблона Module Augmentation для (обновления и) специализации каждой типизации универсальной фабрики для специализированного типа функтора , например, Option . Разве этот шаблон не потребуется для каждого общего использования универсальной фабрики, например, для универсального примера sort который мы обсуждали с

Котлин скопировал вашу идею или наоборот? (некоторые дополнительные критические замечания по этой ссылке, но я не знаю, относятся ли они к случаю Typescript)

Я не хочу быть отрицательным, но это нужно учитывать, если вы собираетесь вкладывать в это ресурсы.

Да, эта мысль тоже пришла мне в голову. Спасибо, что сформулировали это. Я подозреваю, что одним из соображений будет влияние в целом на систему типов и любые угловые случаи, которые она может сгенерировать, как указывает @masaeedu. Возможно, сопротивление возникнет, если это не будет тщательно продумано и продемонстрировано.

Примечание. Я также изучаю Ceylon, чтобы лучше понять, каков будет их уровень инвестиций в цель компиляции EMCAScript. (Мне нужно больше учиться).

Меня тоже только что укусило это ограничение. Я бы хотел, чтобы в следующем примере автоматически выводился I :


interface IdType<T> {
  id: T;
}

interface User {
  id: number;
  name: string;
}

function doStuff<T extends IdType<I>>() {
  const recs = new Map<I, T>();
  return {
    upsert(rec: T) {
      recs.set(rec.id, rec);
    },
    find(id: I) {
      return recs.get(id);
    },
  };
}

(function () {
  const stuff = doStuff<User>();
  stuff.upsert({id: 2, name: "greg"});
  console.log(stuff.find(2));
})();

Насколько я могу судить, для этого требуется более высокородный тип или еще одно указание повторяющегося универсального параметра (например, doStuff<User, number>() ), который кажется избыточным.

Меня недавно тоже поразило это ограничение.

Я работал над библиотекой обещаний . Он предоставляет различные служебные функции для работы с ними.

Центральной особенностью библиотеки является то, что она возвращает обещание того же типа, что и вы в нее вложили. Итак, если вы использовали обещание Bluebird и вызывали одну из функций, оно вернет обещание Bluebird со всеми дополнительными функциями, которые они предоставляют.

Я хотел закодировать это в системе типов, но быстро понял, что для этого требуется работа с типом P вида * -> * таким как P<T> extends Promise<T> .

Вот пример одной из таких функций:

/**
* Returns a promise that waits for `this` to finish for an amount of time depending on the type of `deadline`.
* If `this` does not finish on time, `onTimeout` will be called. The returned promise will then behave like the promise returned by `onTimeout`.
* If `onTimeout` is not provided, the returned promise will reject with an Error.
*
* Note that any code already waiting on `this` to finish will still be waiting. The timeout only affects the returned promise.
* <strong i="14">@param</strong> deadline If a number, the time to wait in milliseconds. If a date, the date to wait for.
* <strong i="15">@param</strong> {() => Promise<*>} onTimeout Called to produce an alternative promise once `this` expires. Can be an async function.
*/
timeout(deadline : number | Date, onTimeout ?: () => PromiseLike<T>) : this;

В приведенной выше ситуации мне удалось избежать необходимости в более высоких типах, используя довольно хакерский тип this .

Однако следующий случай не решается:

/**
* Returns a promise that will await `this` and all the promises in `others` to resolve and yield their results in an array.
* If a promise rejects, the returned promise will rejection with the reason of the first rejection.
* <strong i="21">@param</strong> {Promise<*>} others The other promises that must be resolved with this one.
* <strong i="22">@returns</strong> {Promise<*[]>} The return type is meant to be `Self<T[]>`, where `Self` is the promise type.
*/
and(...others : PromiseLike<T>[]) : ExtendedPromise<T[]>;

Потому что нет никакого хака, который позволил бы мне сделать this<T[]> или что-то в этом роде.

Обратите внимание на мои небольшие извинения в документации.

Есть другой сценарий, в котором, как я считаю, эта функция будет полезна (при условии, что я правильно интерпретирую предложение), как указано в приведенной выше ссылке.

В рассматриваемом пакете необходимо использовать нетипизированный универсальный класс или функцию в качестве типа, поскольку универсальные типы обычно создаются пользователем.

Применяя предложение к моему сценарию, я считаю, что он будет выглядеть примерно так:

import { Component, FunctionalComponent } from 'preact';

interface IAsyncRouteProps {
    component?: Component<~,~> | FunctionalComponent<~>;
    getComponent?: (
        this: AsyncRoute,
        url: string,
        callback: (component: Component<~,~> | FunctionalComponent<~>) => void,
        props: any
    ) => Promise<any> | void;
    loading?: () => JSX.Element;
}

export default class AsyncRoute extends Component<IAsyncRouteProps, {}> {
    public render(): JSX.Element | null;
}

Учитывая, что в моей реализации нет возможности надежно ссылаться на универсальные типы, я уверен, что что-то упустил.

@ Silic0nS0ldier На самом деле этот случай можно решить прямо сейчас. Вы используете тип структурного конструктора, например:

type ComponentConstructor = {
    new<A, B>() : Component<A, B>;
}

А затем произнесите component ?: ComponentConstructor .

Более того, у вас может быть общий тип функции:

let f : <T>(x : T) => T

Это называется параметрическим полиморфизмом ранга n и на самом деле является довольно редкой функцией в языках. Так что еще более загадочно, почему в TypeScript нет высокодородных типов, что является гораздо более распространенной функцией.

Обсуждаемое здесь ограничение появится, если вам нужно указать конкретный TComponent<T, S> . Но в вашем случае это кажется ненужным.


Вы также можете использовать typeof Component который даст вам тип конструктора Component но это вызовет различные проблемы с подтипами.

@ Silic0nS0ldier См. Мои комментарии по сути.

@chrisdavies Это работает?

interface IdType<T> {
    id: T;
}

interface User {
    id: number;
    name: string;
}

function doStuff<T extends IdType<any>>() {
    type I = T['id']; // <==== Infer I
    const recs = new Map<I, T>();
    return {
        upsert(rec: T) {
            recs.set(rec.id, rec);
        },
        find(id: I) {
            return recs.get(id);
        },
    };
}

(function() {
    const stuff = doStuff<User>();
    stuff.upsert({ id: 2, name: "greg" });
    console.log(stuff.find(2));
})();

@ jack-williams Ага. Это работает для моего сценария. Я не нашел этого примера в документации (хотя я известен тем, что чего-то не хватает!). Спасибо!

Я много думал об этой функции, и у меня есть некоторые мысли по этому поводу, склоняясь к какой-то спецификации, но я все еще вижу много проблем. Мои предложения немного отличаются от того, что было предложено до сих пор.


Прежде всего, я думаю, что использование любого синтаксиса T<*, *> для конструкторов типов - плохая идея, потому что они плохо масштабируются со сложностью конструктора типов. Я также не уверен, есть ли смысл указывать тип конструктора типа всякий раз, когда на него ссылаются, поскольку мы не делаем этого для функций, даже для функций с параметрами типа.

Я думаю, что лучший способ реализовать это - обрабатывать высокодородные типы, как и другие типы, с обычными именами и определять хорошее отношение подтипов к самим конструкторам типов, которые можно использовать для наложения ограничений.

Я действительно думаю, что мы должны использовать какой-то префикс или постфикс, чтобы отличать их от других типов, в основном для защиты пользователей от непонятных сообщений об ошибках, связанных с конструкторами типов, когда они только хотели написать обычный код. Мне нравится, как выглядит: ~Type, ^Type, &Type или что-то в этом роде.

Так, например, сигнатура функции может быть такой:

interface List<T> {
    push(x : T);
}

function mapList<~L extends ~List, A, B>(list : L<A>, f : (x : A) => B) : L<B>;

(Я специально не использую префикс ~ для сконструированных типов)

Используя здесь extends я в основном сказал две вещи:

** 1. Если необходимо: ~L - это конструктор типа, который имеет тот же вид, что и ~List , т.е. вид * -> * (или, возможно, * => * , поскольку => - стрелка TypeScript).

  1. ~L - это подтип ~List . **

Использование extends для обозначения типа конструктора типа масштабируется до произвольно сложных конструкторов типов, включая такие вещи, как ((* => *) => (* => *)) => * .

Вы действительно не можете увидеть такой вид в идентификаторе типа, но я не думаю, что вам нужно. Я даже не уверен, что отношение подтипов между конструкторами типов должно сохранять виды, поэтому (1) может не понадобиться.

Не создавать неполные типы

Я считаю, что мы не должны поддерживать построение неполных типов. То есть примерно так:

(*, *) => * => *

Я думаю, это создаст больше проблем, чем того стоит. т.е. каждый конструктор типа должен создавать какой-то конкретный тип, и этот конкретный тип должен быть указан в контексте, в котором определен TC.

Структурный способ определения конструкторов типов

Я также думаю, что должен быть структурный способ определения конструкторов типов, точно так же, как любой другой тип может быть определен структурно, включая универсальные типы функций очень высокого порядка. Я думал о синтаксисе, таком как:

~<A, B> { 
    a : A,
    b : B
}

Это похоже на существующий синтаксис для типов функций с параметрами типа:

<A, B>() => { a : A, b : B};

Их даже можно объединить, чтобы получить следующее:

~<A><B, C> => [A, B, C]

Это конструктор типа, создающий универсальный тип функции.

Преимущество состоит в том, что эти структурные типы могут использоваться при указании других структурных типов, при указании ограничений типа и т. Д. Иногда это означает, что они могут использовать ссылочные локальные символы, на которые нельзя ссылаться из других источников.

Вот пример:

type List<A, B> = ...;

type AdvancedType<~L extends ~<A>List<A, B>, B> = ...;

В приведенном выше примере конструктор структурного типа ~<A>List<A, B> ссылается на параметр типа B . Невозможно указать это отношение каким-либо другим способом, по крайней мере, без кодирования частично сконструированного типа List<A, *> . Есть и другие примеры.

Отношение подтипа

Отношение подтипа кажется логичным, но я столкнулся с рядом трудностей, пытаясь охарактеризовать его.

Моя первая идея была следующей. Чтобы ~A был подтипом ~B :

  1. (а) Они должны быть одного типа (с точки зрения арности, а не ограничений).
  2. (b) Для каждой допустимой параметризации T₁, T₂, ... из ~A , A<T₁, T₂, ...> должен быть подтипом B<T₁, T₂, ...> .

Однако у этого есть несколько ограничений.

  1. класс MySpecialPromise реализует PromiseLike {} В этом случае ~MySpecialPromise не является подтипом ~PromiseLike потому что они имеют разные типы.

  2. класс MyArrayPromiseреализует PromiseLike

    Отношение подтипа в этом случае также не сохраняется.

Более обобщенная версия (b) следующая:

(b) Для каждой допустимой параметризации T₁, T₂, ... из ~A существует такая параметризация S₁, S₂, ... из ~B , что A<T₁, T₂, ...> является подтипом B<S₁, S₂, ...> .

Другими словами, существует отображение F (T₁, T₂, ...) = S₁, S₂, ... с указанными выше свойствами. Это сопоставление необходимо использовать для построения параметризованного B<...> из параметризованного A<...> . Это может позволить нам сделать это, даже если конструкторы типов имеют разные типы.

Проблема с этим отношением в том, что я не уверен, как можно найти правильное сопоставление. В языках с номинальной типизацией каждое утверждение в строках:

A<...> extends B<...>

Определяет соответствие между параметрами типа ~A и параметрами типа ~B , так что это отображение может быть восстановлено. Однако в системе структурной типизации TypeScript мы не можем полагаться на явные инструкции этого типа.

Один из способов - поддерживать конструкторы типов только для типов с правильной информацией о типе, например, предложения implements или какой-то абстрактный член типа, подобный Scala. Однако я не уверен, что это путь вперед.

@GregRos - Интересные заметки! Несколько вопросов.


Что вы подразумеваете под конкретным типом? Вы имеете в виду что-то с видом * или тип без параметров привязанного типа?


Не создавать неполные типы
Я считаю, что мы не должны поддерживать построение неполных типов. То есть примерно так:
(*, *) => * => *

Что вы имеете в виду, говоря о построении неполных типов? Вы имеете в виду, что каждое приложение вроде L<A> должно иметь вид * . Является ли конструктор типа пары особенным в вашем примере, например, подойдет ли (* => *) => * => * ?


Структурный способ определения конструкторов типов

~<A, B> { 
    a : A,
    b : B
}
inferface TyCon<A, B> { 
    a : A,
    b : B
}

Эти примеры отличаются, кроме того, что первый анонимен?


Отношение подтипа

~A и ~B не относятся к типам, поэтому имеет ли смысл для них иметь отношение подтипа? Когда вам действительно нужно проверить, является ли один конструктор «подтипом» другого? Можно ли дождаться применения конструкторов и проверить полученные типы?

@ jack-williams Спасибо за отзыв!

Что вы имеете в виду, говоря о построении неполных типов? Вы имеете в виду, что каждое приложение вроде L<A> должно иметь вид *. Является ли конструктор типа пары особенным в вашем примере, например, подойдет ли (* => *) => * => * ?

Да, точно. Каждое приложение вроде L<A> должно иметь вид * . Я не уверен, насколько я заинтересован в этом.


Эти примеры отличаются, кроме того, что первый анонимен?

Первый - это выражение типа, а второй - объявление. Они идентичны во многих отношениях, так же, как эти типы идентичны во многих отношениях:

{
     a : number;
     b : string;
}

interface Blah {
    a : number;
    b : string;
}

Синтаксис имеет несколько мотивов:

  1. Как и все остальное в TypeScript, он позволяет задавать конструкторы типов структурно и анонимно.
  2. Выражение типа (например, типизированный анонимный объект, упомянутый выше) может использоваться в определенных контекстах, где операторы объявления не могут использоваться, например в сигнатурах типов функций. Это позволяет им фиксировать локальные идентификаторы и выражать вещи, которые нельзя выразить иначе.

~A и ~B не относятся к типам, поэтому имеет ли смысл для них иметь отношение подтипа? Когда вам действительно нужно проверить, является ли один конструктор «подтипом» другого? Можно ли дождаться применения конструкторов и проверить полученные типы?

Конструкторы типов могут или не могут рассматриваться как типы. Я предлагаю рассматривать их как типы, просто неполные, которые не имеют значений и не могут появиться ни в каком контексте, требующем типа значения. Это та же философия, которую использует Scala в этом документе.

Под отношением подтипа я, по сути, подразумеваю своего рода отношение «соответствия», которое можно использовать для ограничения конструкторов типов. Например, если я хочу написать функцию, которая работает со всевозможными обещаниями разных типов, такими как Promise<T> , Bluebird<T> и т. Д., Мне нужна возможность ограничивать параметры TC с помощью interface PromiseLike<T> каким-то образом.

Естественное слово для этого типа отношений - отношение подтипа.

Давайте посмотрим на пример. Предполагая, что мы разработали отношение подтипов между конструкторами типов, я могу написать такую ​​функцию:

function mapPromise<~P extends ~PromiseLike, A, B>(promise : P<A>, func : (x : A) => B) : P<B>;

И ограничение ~P extends ~PromiseLike должно гарантировать, что это функция, которая работает с обещаниями, и только с обещаниями. Ограничение также гарантирует, что внутри тела функции будет известно, что promise реализует PromiseLike<A> и так далее. В конце концов, члены, распознаваемые TypeScript в теле функции, - это именно те члены, существование которых можно доказать с помощью ограничений.

Таким же образом Promise<T> extends PromiseLike<T> , потому что они структурно совместимы и могут быть заменены друг другом, ~Promise extends ~PromiseLike потому что они создают структурно совместимые типы и, таким образом, могут быть заменены друг другом.


Чтобы подчеркнуть проблему с подтипом, подумайте еще раз:

interface MyPromise<T> extends Promise<T[]> {}

Можем ли мы абстрагироваться от ~MyPromise же, как абстрагируем над ~Promise ? Как нам зафиксировать отношения между ними?

Сопоставление, о котором я говорил ранее, - это сопоставление, которое при параметризации ~MyPromise создаст параметризацию ~Promise так что тип, созданный ~MyPromise является подтипом один, созданный ~Promise .

В этом случае отображение выглядит так:

T => T[]

@GregRos

В этом случае ~MySpecialPromise не является подтипом ~PromiseLike потому что они имеют разные типы.

В Haskell проблема такого рода решается путем разрешения частичного применения типов и определения типов таким образом, чтобы последний параметр типа совпадал с параметром типа любого интерфейса, который вы реализуете.

В вашем примере MySpecialPromise будет определено как MySpecialPromise<TSpecial, TPromiseVal> , а ~MySpecialPromise<SpecialType> будет иметь вид, идентичный ~Promise .

@GregRos

Под отношением подтипа я, по сути, подразумеваю своего рода отношение «соответствия», которое можно использовать для ограничения конструкторов типов. Например, если я хочу написать функцию, которая работает со всевозможными обещаниями разных типов, например Promise, Синяя птицаи так далее, мне нужна возможность ограничивать параметры TC с помощью интерфейса PromiseLikeкаким-то образом.
function mapPromise<~P extends ~PromiseLike, A, B>(promise : P<A>, func : (x : A) => B) : P<B> ;

Я думаю, что когда дело доходит до проверки типов, вы попытаетесь объединить BlueBird<T> и PromiseLike<T> для выбранных T , это просто конкретные типы и подпадают под подтипы. Я не понимаю, зачем вам нужны особые отношения для конструкторов ~BlueBird и ~PromiseLike .

Я предполагаю, что это будет использовано в чем-то вроде этого?


let x: <P extends ~PromiseLike>(input : P<A>, func : (x : A) => B) : P<B>;
let y: <P extends ~BlueBird>(input : P<A>, func : (x : A) => B) : P<B>;
x = y;

Здесь вы можете проверить, что ограничения y подразумевают ограничения x, но разве у TypeScript еще нет оборудования, чтобы проверить, что BlueBird<T> extends PromiseLike<T> которое можно использовать?

@ jack-williams Все сводится к тому, как вы указываете следующее ограничение:

~ P - это конструктор типа, такой что для всех A P<A> является подтипом PromiseLike<A> .

Какой синтаксис вы бы использовали? Какую концепцию вы бы использовали? Можно написать примерно так:

function mapPromise<~P, A, B where P<A> extends PromiseLike<A>>

Но у этого синтаксиса есть ограничения. Например, вы вообще не можете выразить этот класс, потому что мы не можем построить тип P<A> в той точке, где он объявлен, чтобы ограничить его:

class PromiseCreator<~P extends ~PromiseLike> {
    create<A>() : P<A>;
}

Но я думаю, вы можете использовать для этого экзистенциальные типы , например:

//Here A is not a captured type parameter
//It's an existential type we introduce to constrain ~P
class PromiseCreator<~P with some A where P<A> extends PromiseLike<A>> {
    create<A>() : P<A>;
}

Затем вы можете потребовать, чтобы все конструкторы типов были ограничены с помощью их сконструированных типов в сигнатуре функции или типа, при необходимости используя экзистенциальные типы.

С экзистенциальными типами это будет иметь ту же выразительную силу, что и отношение подтипа с отображением.

Однако здесь будет несколько проблем:

  1. Указание конструкторов типов с такими видами, как ((* => *) => *) => * , потребует введения множества экзистенциальных типов, некоторые из которых должны быть более высокого порядка. Все они должны присутствовать в сигнатуре функции или класса.
  2. Я не совсем уверен, что было бы легче найти рассматриваемые экзистенциальные типы, чем найти отображение.
  3. Я думаю, что это менее элегантно, чем отношение подтипа.
  4. Потенциально вводит другую форму шрифта, с которой вам придется иметь дело.

@GregRos

Какой синтаксис вы бы использовали? Какую концепцию вы бы использовали?

_Лично_ я бы не стал использовать какой-либо специальный синтаксис и просто использовал:

function mapPromise<P extends PromiseLike, A, B>(p: P<A>, f: (x: A) => B): P<B>

class PromiseCreator<P extends PromiseLike> {
    create<A>() : P<A>;
}

но это всего лишь мое мнение, поскольку я рассматриваю такие вещи, как number как конструктор с нулевым значением: поэтому здесь не должно быть различий.

Я считаю, что создание подтипов для функций-конструкторов должно быть максимально простым. Они должны иметь одинаковую арность, а параметры должны быть разновидностями друг друга с учетом контравариантности и ковариации, как в документе Scala.

Частичное приложение может обойти случаи, когда они имеют разную арность (я бы не возражал против автоматического карринга для конструкторов типов, поэтому вы можете просто написать MySpecialPromise<SpecialType> ).

В примере interface MyPromise<T> extends Promise<T[]> {} я должен быть честным и сказать, что я не уверен, что такая сложность стоит того, чтобы справиться с этим случаем - я думаю, что это было бы достаточно полезной функцией без этого.

Обработка этого случая эквивалентна (я думаю), говоря: ~MyPromise extends ~(Promise . []) где [] - это конструктор списка, а . - композиция конструктора. Похоже, что все становится намного сложнее, поскольку теперь недостаточно просто проверить структуру конструкторов, но вы также должны рассуждать о композиции!

@ jack-williams Это не работает с параметрами типа по умолчанию. Если я напишу P extends Foo , где Foo имеет параметр типа по умолчанию, то есть type Foo<T = {}> = ... , тогда что это за P ?

Я просто хочу сказать, что одобряю типы более высокого порядка (у меня были ситуации в реальных проектах TypeScript, когда они были бы полезны).

Однако я не думаю, что они должны поддерживать каррирование. Я люблю Haskell, но это не подходит TypeScript.

Типы более высокого порядка полезны даже без каррирования или частичного применения, но если требуется частичное приложение, я бы предпочел увидеть для него явный синтаксис. Что-то вроде этого:

Foo<number, _>  // equivalent to `type Foo1<A> = Foo<number, A>`

@ Кэмерон-Мартин

Изменить: извините, я не думаю, что мои комментарии были _не_ очень четкими. Под P есть свой собственный вид, я имею в виду, что он имеет вид, навязанный ему своим использованием. Скажем, ограничения всегда считаются наивысшими, поэтому Foo предполагается равным ~Foo . Только если мы заставим P быть более низким, мы проверим, есть ли в Foo параметр по умолчанию. Меня беспокоит этот вывод, но в этом случае ~ поможет, и я думаю, нам нужны полные аннотации.

P есть свои, не так ли? Будет ли этот вопрос не будет делать лечим Foo в ~Foo , или как Foo<{}> : Я бы сказал , что будет определяться по виду P. Так что если P - это тип, для которого мы принудительно используем параметр по умолчанию, и если P является конструктором * => * , то мы относимся к Foo же.

@Pauan Согласитесь с вашими предложениями.

@ jack-williams Я рассмотрел понятие подтипов, как упоминал ранее:

Моя первая идея была следующей. Чтобы ~A был подтипом ~B :

  1. (а) Они должны быть одного типа (с точки зрения арности, а не ограничений).
  2. (b) Для каждой допустимой параметризации T₁, T₂, ... из ~A , A<T₁, T₂, ...> должен быть подтипом B<T₁, T₂, ...> .

Проблема в том, что если мы будем делать вещи настолько простыми, насколько это возможно, мы получим парадоксальное отношение подтипа, которое не вписывается в язык.

Если MyPromise<T> extends Promise<T[]> это означает, что MyPromise<T> нужно использовать везде, где можно использовать Promise<T[]> , но это уже не так.

Если вы использовали as для преобразования a : MyPromise<T> в Promise<T[]> , вы бы повысили качество, но это парадоксальным образом сделало бы a более назначаемым.

Существующие общие ограничения, которые соответствуют существующему отношению подтипа, также могут использоваться для достижения аналогичного эффекта и вызывать странное поведение:

function id1<A, ~P extends ~PromiseLike>(p : P<A>) : P<A>;

function id2<A, P extends Promise<A[]>>(p : P) : P {
    //ERROR - P does not extend PromiseLike<A>
    return id1(p);
}

В качестве побочного эффекта набор текста станет хотя бы частично номинальным. Эти типы внезапно стали бы другими, если бы они были такими же:

type GenericNumber<T> = number;

type RegularNumber = number;

Я даже не уверен, какое влияние это окажет на сложные типы объединения / пересечения с параметрами типа, чисто структурные типы, типы с пониманием членов и тому подобное.

Мое личное мнение таково: отношение подтипа к конструкторам типов должно уважать существующее, а не противодействовать ему . К сожалению, это требует более сложных действий.


Самая большая причина использования какой- то специальной нотации для конструкторов типов состоит в том, что 99% разработчиков не знают, что такое конструктор типов, и не хотели бы получать сообщения об ошибках, связанных с ними.

Это сильно отличается от Haskell, где каждый разработчик по закону должен пройти углубленный курс теории категорий.

Вторичная причина заключается в том, что в некоторых случаях (например, в вышеупомянутом случае параметров по умолчанию) синтаксис будет либо неоднозначным, либо вообще невозможно будет абстрагироваться от конструктора определенного типа.

РЕДАКТИРОВАТЬ: Извините, @GregRos, я не видел ваших последних комментариев!

Отношение подтипа к конструкторам типов должно уважать существующее, а не противоречить ему.

Если это удастся, я согласен. Я просто не вникаю во все детали и насколько легко это будет.

Вторичная причина заключается в том, что в некоторых случаях (например, в вышеупомянутом случае параметров по умолчанию) синтаксис будет либо неоднозначным, либо вообще невозможно будет абстрагироваться от конструктора определенного типа.

Я не уверен, что согласен с тем, что было бы неоднозначно, если бы вы всегда предполагали наивысший вид ограничения, пока вам не понадобится его меньшее значение. Это не утверждение, и если есть другие примеры, которые показывают обратное, тогда достаточно справедливо.


Проблема в том, что если мы будем делать вещи настолько простыми, насколько это возможно, мы получим парадоксальное отношение подтипа, которое не вписывается в язык.

Это может быть правдой, я думаю, меня просто беспокоит, возможно ли реализовать альтернативу. Как бы то ни было, если бы работало более сложное решение, было бы здорово!

Имея более общее представление о подтипах, которое показывает существование функции отображения, кажется, трудно реализовать в целом. Правильно ли интерпретируются мои правила в моем следующем примере?

(b) Для любой допустимой параметризации T₁, T₂, ... ~ A существует параметризация S₁, S₂, ... ~ B такая, что A

Будет ли X подтипом Y в следующем случае, учитывая отображение F (A, B) = (number, B).

type X = ~<A,B> = {x : B};
type Y = ~<A,B> = A extends number ? {x: B} : never;

Однако X<string,number> не будет подтипом Y<string,number> .

Думаю, мне не ясно, достаточно ли _существования_ сопоставления. Если мы возьмем ~ A и ~ B как функции и хотим показать, что ~ B приближает ~ A, или ~ A является подтипом ~ B, то показывая, что существует некоторая функция ~ C, такая, что ~ A является подтипа (~ B. ~ C), я думаю, недостаточно (C - преобразователь). Я должен относиться к _все_ сопоставлениям.

function id1<A, ~P extends ~PromiseLike>(p : P<A>) : P<A>;

function id2<A, P extends Promise<A[]>>(p : P) : P {
    //ERROR - P does not extend PromiseLike<A>
    return id1(p);
}

Я не совсем следую этому примеру, должна ли здесь не произойти ошибка? Я считаю, что id1 должен иметь вход, созданный функцией P которая дает PromiseLike для всех _inputs_. В то время как id2 говорит о значении, которое должно быть подтипом применения обещания к A []. Я не уверен, можно ли восстановить информацию, необходимую для id1 , из типа id2 . Я думаю, что могу неправильно понять вашу точку зрения.

Эти типы внезапно стали бы другими, если бы они сейчас были одинаковыми.

Опять же, я боюсь, что могу упустить вашу точку зрения, но он не знает, как они совпадают. Я не могу заменить RegularNumber на GenericNumber в типе, мне пришлось бы дать последнему аргумент.

Думаю, мне не ясно, достаточно ли существования сопоставления. Если мы возьмем ~ A и ~ B как функции и хотим показать, что ~ B приближает ~ A, или ~ A является подтипом ~ B, то показывая, что существует некоторая функция ~ C, такая, что ~ A является подтипа (~ B. ~ C), я думаю, недостаточно (C - преобразователь). Я должен относиться ко всем сопоставлениям.

Да, вы правы, и это контрпример, который вы привели. Я нашел другие контрпримеры. Совсем не работает.

Я перечитал эту ветку и множество ваших ответов. Я думаю, что вы правы во многих вещах, и я неправильно смотрел на проблему. Я пойму, что имею в виду.

Я не уверен, что согласен с тем, что было бы неоднозначно, если бы вы всегда предполагали наивысший вид ограничения, пока вам не понадобится его меньшее значение. Это не утверждение, и если есть другие примеры, которые показывают обратное, тогда достаточно справедливо.

Это либо неоднозначно, либо становится невозможным ссылаться на что-то. Как и в примере выше, на конструктор типа Foo становится невозможно ссылаться, поскольку он скрыт самим типом. Если вы напишете ~Foo или, если на то пошло, Foo<*> или ~<A>Foo<A> или что-нибудь еще, что не конфликтует с другими вещами, у вас не будет такой проблемы.

Да, вы можете обойти это, определив псевдоним, хотя это не очень красиво:

type Foo2<T> = Foo<T>

Как я уже сказал, я не думаю, что это самая важная проблема.

Я не совсем следую этому примеру, должна ли здесь не произойти ошибка? Я считаю, что id1 должен иметь вход, созданный функцией P, которая дает PromiseLike для всех входов. В то время как id2 говорит о значении, которое должно быть подтипом применения Promise к A []. Я не уверен, можно ли восстановить информацию, необходимую для id1, из типа id2. Я думаю, что могу неправильно понять вашу точку зрения.

Это правильное чтение, да. Но если P extends Promise<A[]> его следует назначить любому месту, которое принимает Promise<A[]> , например id1 . Вот как это обстоит сейчас и что означает подтипирование.

Я действительно не думаю, что этого можно избежать.

Опять же, я боюсь, что могу упустить вашу точку зрения, но он не знает, как они совпадают. Я не могу заменить RegularNumber на GenericNumber в типе, мне пришлось бы дать последнему аргумент.

Я имел в виду следующее: тип GenericNumber<T> для всех T и тип RegularNumber идентичны и взаимозаменяемы. Нет контекста, в котором один вводил бы проверку, а другой - нет. По крайней мере, прямо сейчас.

То, о чем мы говорили, сделало бы их другими. Поскольку GenericNumber<T> от TC, его можно будет использовать там, где RegularNumber не может быть. Так что он больше не будет взаимозаменяемым.

Я думал об этом и полагаю, что это может быть неизбежно и не обязательно плохо. Просто новое, другое поведение.

Один из способов подумать об этом состоит в том, что параметр типа становится частью «структуры» типа.

Я действительно думаю, что ведущие участники будут вести себя иначе.

Новое направление

Прежде всего, я думаю, вы правы в том, что правильное отношение подтипа - это то, для которого нет сопоставления:

Моя первая идея была следующей. Чтобы ~A был подтипом ~B :

  1. (а) Они должны быть одного типа (с точки зрения арности, а не ограничений).
  2. (b) Для каждой допустимой параметризации T₁, T₂, ... из ~A , A<T₁, T₂, ...> должен быть подтипом B<T₁, T₂, ...> .

Картография ... честно говоря, это довольно глупо. Я не думаю, что есть способ объединить MyPromise<T> extends Promise<T[]> и ~Promise . Я хотел бы знать, думает ли кто-нибудь иначе.

Я также хотел бы знать, есть ли пример, который мне не хватает, где даже это правило не работает.

Если мы согласны с тем, что ограничения конструктора типов должны быть выражены с помощью отношения подтипа, которое действительно работает очень хорошо, мы можем перейти к другим вещам.

О синтаксисе

Похоже, мы не совсем согласны по этому поводу. Я настоятельно предпочитаю синтаксис префикса, подобный ~Promise . Концептуально ~ можно рассматривать как «ссылку на оператор TC of» или что-то в этом роде.

Думаю, я привел несколько причин, почему это лучше альтернатив:

  1. Совершенно однозначно.

    1. Ошибки, связанные с этим синтаксисом, также однозначны. Если кто-то забудет параметризовать тип, он не получит ошибок в конструкторах типов, если не знает, что они собой представляют.

    2. Как побочный эффект, существующие тексты ошибок и логику изменять не нужно. Если кто-то напишет Promise сообщение об ошибке будет точно таким же, как сейчас. Не нужно ничего менять, чтобы говорить о TC.

  2. Хорошо расширяется до структурного синтаксиса.
  3. Я считаю, что его легко разобрать. Предполагается, что любые символы ~\w появляющиеся там, где ожидается тип, указывают на ссылку на TC.
  4. Легко печатать.

Я надеюсь, что другие люди могут высказать свое мнение.

Об объединениях, пересечениях и параметрах типа по умолчанию

Допустимы ли перегруженные / смешанные виды форм * & (* => *) , * | (* => *) и т. Д.? Есть ли у них интересное применение?

Я думаю, что это плохая идея, и о них трудно рассуждать. Я также не уверен, какая аннотация типа вам понадобится для устранения неоднозначности * | (* => *) чтобы вы могли построить из нее тип.

Можно сказать, что прямо сейчас такие типы существуют, это типы с параметрами типа по умолчанию:

type Example<A = number> = {}

Можно сказать, что этот тип имеет вид * & (* => *) потому что он может принимать параметр типа для создания типа, но это не обязательно.

Я считаю, что параметры типа по умолчанию должны быть сокращением, а не способом описания типов. Поэтому я думаю, что параметры типа по умолчанию следует просто игнорировать при определении типа типа.

Однако, возможно, имеет смысл поговорить о таких типах, как ~Promise | ~Array . Они имеют одинаковый вид, поэтому они совместимы. Думаю, это следует поддержать.

Вещи, с которыми придется иметь дело

Придется разобраться со связанными ситуациями, например, с этой ситуацией:

type Example = (<~P extends ~Promise>() => P<number>) | (<~M extends ~Map>() => Map<string, number>);

Но на самом деле это не (* => *) | (*, *) => * , а что-то другое

Создание других ТК?

Как я уже упоминал ранее, я не считаю хорошей идеей иметь TC, которые создают другие TC, например * => (* => *) . Они являются нормой для языков, поддерживающих каррирование и т.п., но не для TypeScript.

Я не могу вообще определить такие типы с помощью синтаксиса ~ и отношения подтипов, поэтому никаких специальных правил для их запрета не требуется. Чтобы заставить их работать, потребуются особые правила.

Я думаю, вы могли бы определить их структурно следующим образом:

~<A>~<B>{a : A, b : B}

Я думаю, что это почти единственный способ.

Взаимодействие с функциями более высокого ранга?

Существует естественное, но сложное взаимодействие с типами функций, которые принимают параметры типа:

type Example<T> = <~P extends ~Promise>(p : P<T>) : P<T>;

Следует как-то прекратить это взаимодействие? Я вижу, как эти типы становятся очень сложными.

А вообще есть ли места, где не должны появляться параметры TC?

Подходит ли мой структурный синтаксис?

Я не думаю, что это нужно реализовывать сразу, но я все же думаю, что мой структурный синтаксис - хорошая идея. Это позволяет вам:

  1. Используйте локальные идентификаторы, как и другие параметры типа, в ваших ограничениях.
  2. Частичное применение конструкторов типов очень явным и гибким способом: ~<A>Map<string, A> , ~<A, B>Map<B, A> и так далее.
  3. Каждый другой аспект типа имеет структурный синтаксис, поэтому TC также должны иметь такой синтаксис.

Тем не менее, TC могут полностью работать без этого, и первый PR, вероятно, их не затронет.

Взаимодействие с условными типами

Как функция будет работать с условными типами? Сможете ли вы это сделать?

type Example<~P extends ~PromiseLike> = ~P extends ~Promise ? 0 : 1

Я и сам не совсем уверен. Все еще не полностью переварены условные типы.

Разрешение перегрузки

У меня такое чувство, что это будет сложно сделать. На самом деле это очень важно, потому что разные формы разрешения перегрузки в конечном итоге приведут к созданию разных типов.

Тем не менее, сейчас я не могу привести никаких хороших примеров.

Вы знаете, многое из этого было бы спорным, если бы четко определенный промежуточный язык использовался для описания TypeScript в качестве отправной точки. Например: System F <: или одна из систем звуко-зависимых типов, таких как Simplified Dependent ML .

Я был бы искренне удивлен, если бы это разрешилось раньше
https://github.com/Microsoft/TypeScript/issues/14833

Я думаю, что # 17961 может решить эту проблему косвенно. См. Эту суть для более подробной информации.

Обратите внимание, что типы Bifunctor и Profunctor немного сложны на уровне ограничений - было бы намного проще, если бы у меня были очевидные универсальные типы для работы, а не infer T который ограничен только условными типами. Кроме того, было бы неплохо, если бы я мог использовать this в качестве типа "возврата" (то есть чисто уровня типа) - это упростило бы определение большинства моих интерфейсов.

(Я не заядлый пользователь TS, поэтому, возможно, совершил ошибки.

@isiahmeadows @ tycho01 Вау ...

Вы правы. Если я правильно понимаю, результаты примерно такие же.

Есть несколько отличий. Но функционально они практически идентичны, и я думаю, что эти различия можно устранить.

Невозможно вывести функцию правильного типа

function example<~P extends ~PromiseLike>(p : P<number>) : P<string>;

Здесь вы можете вывести ~Promise и ~Bluebird из p . Однако если вы сделаете это так:

function example<F extends <T>(t: T) => PromiseLike<T>>(p : F(number)) : F(string)

Я очень сомневаюсь, что это сработает:

example(null as Promise<number>)

Невозможно сделать вывод, что F должно быть:

<T>(t : T) => Promise<T>

Потому что эта функция ни в коем случае не считается особенной. Принимая во внимание, что с TC, некоторые типы по существу имеют "неявную" функцию уровня типа: их TC.

Невозможно легко сослаться на существующие ведущие участники

Вы не можете сделать ~Promise как в моем предложении. Вам нужно будет напрямую закодировать тип, используя структурный формат:

type PromiseTC = <T>() => Promise<T>

Верно, и это вызывает беспокойство. Это скорее проблема вывода типа, когда вам нужна возможность вывести саму универсальную функцию на основе известного типа аргумента (обратное тому, что обычно происходит). Его можно решить достаточно общим способом, чтобы работать в большинстве случаев, но для этого требуется новый особый случай, который нетривиален.

Это может быть частично решено за счет стратегического использования NoInfer<T> , но я не уверен на 100%, как это нужно сделать, и насколько это может решить даже общий случай.

@GregRos

Я не сильно поддерживаю какой-либо синтаксис, это просто мое предпочтение, у ~ есть множество достоинств. Я думаю, что главное, что следует учитывать, - это то, требуется ли синтаксис для явных аннотаций типа, потому что вывод не всегда возможен.


Картография ... честно говоря, это довольно глупо. Я не думаю, что есть способ объединить MyPromiseрасширяет обещание

Я думаю, что сопоставление все еще может быть полезным понятием, но в приведенном выше случае я не думаю, что мы когда-либо должны пытаться объединить ~MyPromise с ~Promise , то, что нужно объединить это ~MyPromise и ~<T>Promise<T[]> , которые мы также могли бы написать ~(Promise . []) . Я думаю, чего не хватало, так это того, что отображение должно быть частью отношения подтипов: это такая же часть конструктора, как Promise . В этом примере отображение - это просто конструктор списка.

interface A<T> {
    x: T;
} 

interface B<T> {
    x: T[];
}

~<T>B<T> расширяет ~<T>A<T[]> ? Да. ~<T>B<T> расширяет ~<T>A<T> ? Нет. Но в конечном итоге это два не связанных между собой вопроса.

Если мы согласны с тем, что ограничения конструктора типов должны быть выражены с помощью отношения подтипа, которое действительно работает очень хорошо, мы можем перейти к другим вещам.

Да, я думаю, это хороший способ описания вещей.


Невозможно вывести функцию правильного типа

function example<~P extends ~PromiseLike>(p : P<number>) : P<string>;
Здесь вы можете вывести ~Promise и ~Bluebird из p.

Это не утверждение, а скорее открытый вопрос, поскольку я не совсем уверен, как работает проверка типов. Я думал, что, используя интерфейс A выше в качестве примера, типы A<number> и {x: number} неотличимы, и поэтому я не уверен, что можно будет вывести конструктор из типа, возвращенного приложением конструктора. Можно ли восстановить P из P<number> ? Я уверен, что все можно изменить, чтобы поддержать это, мне просто интересно, что он делает сейчас.

Перекрестный ответ от # 17961, но, к сожалению, я не уверен, как заставить подход @isiahmeadows работать. Я боюсь, что обратный вывод о вызовах типов нетривиален.

Таким образом, похоже, что на основе ввода Promise<number> или Bluebird<number> мы хотим иметь возможность вывести непримененные версии этих типов, чтобы мы могли повторно применить их, например, с помощью string . Хотя это звучит сложно.

Даже если типы ввода похожи на эти вместо некоторого структурного эквивалента (мы являемся языком со структурной типизацией, верно?), Это рассуждение также становится туманным, если, например, Bluebird имеет два типа параметров вместо этого, и в этот момент наш <string> Приложение
Я не уверен, что там есть хорошее решение. (отказ от ответственности: я немного отстал в обсуждении здесь.)

@ tycho01 Все эти проблемы исчезнут, если люди явно T ?

Я думаю, что это разумно, учитывая, что я сомневаюсь, что вывод может быть решен для всех случаев в любом случае.

@ jack-williams: пока не с # 17961, но я думаю, что использование его для отправки может помочь:

let arr = [1, 2, 3];
let inc = (n: number) => n + 1;
let c = arr.map(inc); // number[]
let map = <Functor extends { map: Function }, Fn extends Function>(x: Functor, f: Fn) => x['map'](f); // any on 2.7 :(
let e = map(arr, inc);

@ tycho01 Да, я понял, что мое предложение ужасно, потому что T не создается при вызове метода.

Хотели бы что-нибудь сделать следующее?

interface TyCon<A> {
    C: <A>(x: A) => TyCon<A>
}

interface Functor<A> extends TyCon<A> {
    C: <A>(x: A) => Functor<A>;
    fmap<B>(this: this["C"](A), f: (x: A) => B): this["C"](B);
}

interface Option<A> extends Functor<A> {
    C: <A>(x: A) => Option<A>;
}

@ jack-williams Думаю, вопрос должен заключаться в том, как он будет сравниваться по поведению с реализацией ADT в fp-ts , но похоже, что это может сработать, да. Наверное, и без TyCon .

@ Джек-Уильямс @isiahmeadows :

Я попробовал эту идею в потоке попыток , поскольку в нем уже есть $Call . Для меня вроде как-то перестало отвечать ...

interface Functor<A> {
    C: <A>(x: A) => Functor<A>;
    fmap<B>(f: (x: A) => B): $Call<$ElementType<this, "C">, B>;
}
// this: $Call<$ElementType<this, "C">, A>, 
// ^ flow doesn't seem to do `this` params

interface Option<A> extends Functor<A> {
    C: <A>(x: A) => Option<A>;
}

let o: Option<string>;
let f: (s: string) => number;
let b = o.fmap(f);

@ tycho01 Я думаю, что нельзя просто получить свойства с $ElementType из this в потоке

@ tycho01 вы не можете заставить это работать и в машинописном тексте
детская площадка: https://goo.gl/tMBKyJ

@goodmind : хм, похоже, он выводит Maybe<number> вместо Functor<number> после копирования fmap из Functor в Maybe .
Я полагаю, что с вызовами типов это улучшило бы наличие только типа, вместо того, чтобы нуждаться в реализации этого типа во время выполнения.
Теперь функторам уже потребуются собственные реализации fmap . Однако это было бы отстойно для производных методов .
Возвращается на круги своя. : /

Некоторые соответствующие идеи можно найти на https://github.com/SimonMeskens/TypeProps/issues/1.

Я планирую выпустить альфа-версию как можно скорее, но вы можете вместе со мной писать примеры в этом выпуске, чтобы уже почувствовать это.

Эта конкретная проблема немного длинна, чтобы разбираться полностью, но то, что я ищу, содержит простые, но реальные примеры кода, который вы не можете набрать из-за отсутствия параметризованных универсальных типов. Я думаю, что могу напечатать большинство из них (при условии, что они не полагаются на абстрактные конструкторы с поднятыми типами). Не стесняйтесь открывать проблемы в репозитории выше с кодом, и я напечатаю их для вас, если смогу (или вы также можете опубликовать их здесь).

Внимание! Я начал попытку реализовать это на # 23809. Он все еще не завершен, но проверьте его, если вам интересно.

Я обещал вам простой пример, вот он. Здесь используются несколько приемов, которые я узнал при написании своей библиотеки.

type unknown = {} | null | undefined;

// Functor
interface StaticFunctor<G> {
    map<F extends Generic<G>, U>(
        transform: (a: Parameters<F>[0]) => U,
        mappable: F
    ): Generic<F, [U]>;
}

// Examples
const arrayFunctor: StaticFunctor<any[]> = {
    map: <A, B>(fn: (a: A) => B, fa: A[]): B[] => {
        return fa.map(fn);
    }
};
const objectFunctor: StaticFunctor<object> = {
    map: <A, B>(fn: (a: A) => B, fa: A): B => {
        return fn(fa);
    }
};
const nullableFunctor: StaticFunctor<object | null | undefined> = {
    map: <A, B>(
        fn: (a: A) => B,
        fa: A | null | undefined
    ): B | null | undefined => {
        return fa != undefined ? fn(fa) : fa;
    }
};

const doubler = (x: number) => x * 2;

const xs = arrayFunctor.map(doubler, [4, 2]); // xs: number[]
const x = objectFunctor.map(doubler, 42); // x: number
const xNull = nullableFunctor.map(doubler, null); // xNull: null
const xSome = nullableFunctor.map(doubler, 4 as number | undefined); // xSome: number | undefined

const functor: StaticFunctor<unknown | any[]> = {
    map(fn, fa) {
        return Array.isArray(fa)
            ? arrayFunctor.map(fn, fa)
            : fa != undefined
                ? objectFunctor.map(fn, fa)
                : nullableFunctor.map(fn, fa);
    }
};

const ys = functor.map(doubler, [4, 2]); // ys: number[]
const y = functor.map(doubler, 42); // y: number
const yNull = functor.map(doubler, null); // yNull: null
const ySome = functor.map(doubler, 42 as number | undefined); // ySome: number | undefined

// Plumbing
interface TypeProps<T = {}, Params extends ArrayLike<any> = never> {
    array: {
        infer: T extends Array<infer A> ? [A] : never;
        construct: Params[0][];
    };
    null: {
        infer: null extends T ? [never] : never;
        construct: null;
    };
    undefined: {
        infer: undefined extends T ? [never] : never;
        construct: undefined;
    };
    unfound: {
        infer: [NonNullable<T>];
        construct: Params[0];
    };
}

type Match<T> = T extends infer U
    ? ({} extends U ? any
        : TypeProps<U>[Exclude<keyof TypeProps, "unfound">]["infer"]) extends never
    ? "unfound"
    : {
        [Key in Exclude<keyof TypeProps, "unfound">]:
        TypeProps<T>[Key]["infer"] extends never
        ? never : Key
    }[Exclude<keyof TypeProps, "unfound">] : never;


type Parameters<T> = TypeProps<T>[Match<T>]["infer"];

type Generic<
    T,
    Params extends ArrayLike<any> = ArrayLike<any>,
    > = TypeProps<T, Params>[Match<T>]["construct"];

Я обновил и упростил образец, вот и ссылка на игровую площадку:
Игровая площадка

Я добавил библиотеку NPM для вышеупомянутого, чтобы вам было проще с ней работать. В настоящее время в стадии альфа-тестирования, пока я не получу должное тестирование, но это должно помочь вам, ребята, пытающимся писать HKT.

Ссылка на Github

Я играл с простым подходом к моделированию HKT, используя условные типы для замены переменных виртуального типа внутри насыщенного типа:

declare const index: unique symbol;

// A type for representing type variables
type _<N extends number = 0> = { [index]: N };

// Type application (substitutes type variables with types)
type $<T, S, N extends number = 0> =
  T extends _<N> ? S :
  T extends undefined | null | boolean | string | number ? T :
  T extends Array<infer A> ? $Array<A, S, N> :
  T extends (x: infer I) => infer O ? (x: $<I, S, N>) => $<O, S, N> :
  T extends object ? { [K in keyof T]: $<T[K], S, N> } :
  T;

interface $Array<T, S, N extends number> extends Array<$<T, S, N>> {}

// Let's declare some familiar type classes...

interface Functor<F> {
  map: <A, B>(fa: $<F, A>, f: (a: A) => B) => $<F, B>;
}

interface Monad<M> {
  pure: <A>(a: A) => $<M, A>;
  bind: <A, B>(ma: $<M, A>, f: (a: A) => $<M, B>) => $<M, B>;
}

interface MonadLib<M> extends Monad<M>, Functor<M> {
  join: <A>(mma: $<M, $<M, A>>) => $<M, A>;
  // sequence, etc...
}

const Monad = <M>({ pure, bind }: Monad<M>): MonadLib<M> => ({
  pure,
  bind,
  map: (ma, f) => bind(ma, a => pure(f(a))),
  join: mma => bind(mma, ma => ma),
});

// ... and an instance

type Maybe<A> = { tag: 'none' } | { tag: 'some'; value: A };
const none: Maybe<never> = { tag: 'none' };
const some = <A>(value: A): Maybe<A> => ({ tag: 'some', value });

const { map, join } = Monad<Maybe<_>>({
  pure: some,
  bind: (ma, f) => ma.tag === 'some' ? f(ma.value) : ma,
});

// Not sure why the `<number>` annotation is required here...
const result = map(join<number>(some(some(42))), n => n + 1);
expect(result).toEqual(some(43));

Проект здесь: https://github.com/pelotom/hkts

Обратная связь приветствуется!

@pelotom Мне нравится легкость синтаксиса вашего подхода. Есть два других подхода, которые еще не упоминались в этой ветке, которые могут стимулировать творческий подход к созданию текущих и будущих решений. Оба являются объектно-ориентированными решениями этой проблемы.

  1. Бертран Мейер описал способ моделирования универсальных типов в своей книге 1988 года «Конструирование объектно-ориентированного программного обеспечения».

Примеры приведены на языке Eiffel, но приблизительный перевод на TypeScript выглядит так:

https://gist.github.com/mlhaufe/089004abd14ad8e7171e2a122198637f

Вы заметите, что они могут стать довольно тяжелыми из-за необходимости в промежуточных представлениях классов, но с формой фабрики классов или с подходом TypeScript Mixin это можно значительно уменьшить.

Может быть некоторая применимость к # 17588

  1. Второй подход используется при моделировании объектных алгебр (и абстрактных фабрик):

C<T> представлен как App<t,T> где T - это класс, а t - уникальный тег, связанный с C

interface App<C,T> {}

Образец:

interface IApp<C,T> {}

interface IList<C> {
    Nil<T>(): IApp<C,T>
    Cons<T>(head: T, tail: IList<C>): IApp<C,T>
}

// defining data
abstract class List<T> implements IApp<typeof List, T> {
    // type-safe down-cast
    static prj<U>(app: IApp<typeof List, U>): List<U> { return app as List<U> }
}
class Nil<T> extends List<T> { }
class Cons<T> extends List<T> {
    constructor(readonly head: T, readonly tail: List<T>) {
        super()
    }
}

// The abstract factory where the HKT is needed
class ListFactory<T> implements IList<typeof List> {
    Nil<T>(): IApp<typeof List, T> { return new Nil() }
    Cons<T>(head: T, tail: IApp<typeof List, T>): IApp<typeof List, T> {
        return new Cons(head, tail)
    }
}

Вы можете увидеть более подробную информацию и обоснование в следующей статье в разделе 3.5 «Эмуляция полиморфизма конструктора типов»:

https://blog.acolyer.org/2015/08/13/streams-a-la-carte-extensible-pipelines-with-object-algebras/

@metaweta , не могли бы вы переименовать эту проблему в Higher kinded types in TypeScript чтобы она лучше видна в поиске Google?

Возможно, наши мудрые и доброжелательные специалисты по сопровождению репозитория (например, @RyanCavanaugh , @DanielRosenwasser ) смогут отредактировать заголовок, если такое изменение будет сочтено достойным их вмешательства?

Мне любопытно узнать, что означает, что это было перемещено из сообщества в очередь. Является ли это тем, что основная команда сейчас более серьезно рассматривает, или это просто означает, что команда решила, что это не лучший кандидат для сообщества?

Нашел: веха «Сообщество», по-видимому, устарела в пользу «Бэклога» , поэтому эта проблема, вероятно, была перенесена в натуральном виде.

Не член TS, просто кто-то, кто решил перейти по ссылке, по которой он был переименован.

+1

Вот кое-что, что я как раз пытался построить, что кажется действительно практическим случаем для высокородных типов.

Я хочу создать абстракцию для базы данных, которая может работать синхронно или асинхронно. Вместо того, чтобы использовать обратные вызовы и взломать это, я хотел использовать общий. Вот что я хотел бы сделать:

type Identity<T> = T

interface DatabaseStorage<Wrap<T> extends Promise<T> | Identity<T>> {
    get(key: string): Wrap<any>
    set(key: string, value: any): Wrap<void>
}

Это было бы действительно мощно!

@ccorcos это называется MTL-стилем. Вы можете взглянуть на https://github.com/gcanti/fp-ts/blob/master/tutorials/mtl.ts для чисто функционального примера с fp-ts .

@mlegenhausen Мне очень жаль, но мне трудно следовать этому примеру.

Всякий раз, когда я копаюсь в fp-ts , я беспокоюсь, что все становится настолько сложно, что может стать хрупким. Хотя пример

По какой причине это не было принято в TypeScript?

@ccorcos IMHO, даже когда я рекомендовал пример из fp-ts я бы вообще не рекомендовал стиль MTL / без тегов. Вы добавляете дополнительный уровень абстракции к каждой эффективной монаде, которой вам нужно управлять вручную, потому что машинописный текст не может определить, какую монаду вы хотите использовать, и здесь все усложняется. Что я вижу в сообществе fp-ts это использование одной асинхронной монады (я бы порекомендовал TaskEither ) и придерживаться ее. Даже при тестировании преимущества MTL не стоят тех хлопот, которые вы получаете в своем не тестирующем коде. Hyper-ts на основе fp-ts - это один из примеров библиотеки, которая недавно отказалась от поддержки MTL.

Интересно ... hyper-ts выглядит круто ...

Я придумал легкую кодировку типов более высокого порядка на основе F-ограниченного полиморфизма: https://github.com/strax/tshkt

Преимущество этого подхода состоит в том, что вам не нужна таблица поиска (единственный условный тип или объект со строковыми ключами) для связывания конструкторов типов с типами. Этот метод также можно использовать для кодирования приложений универсальных функций уровня типа (подумайте, ReturnType<<T>(value: T) => Array<T>> ).

Это все еще проверка концепции, поэтому мы будем очень благодарны за отзывы о жизнеспособности этого подхода!

Я посмотрю на этот @strax , он выглядит действительно круто!

А пока вот глупый пример того, что мы можем сделать сейчас:

type Test1 = λ<Not, [True]>;        // False
type Test2 = λ<And, [True, False]>; // False
type Test3 = λ<And, [True, True]>;  // True

// Boolean

interface True extends Func {
    expression: Var<this, 0>;
}

interface False extends Func {
    expression: Var<this, 1>;
}

interface Not extends Func {
    expression: λ<Var<this, 0>, [False, True]>
}

interface And extends Func {
    expression: λ<Var<this, 0>, [Var<this, 1>, Var<this, 0>]>
}

// Plumbing

type Func = {
    variables: Func[];
    expression: unknown;
}

type Var<F extends Func, X extends number> = F["variables"][X];

type λ<Exp extends Func, Vars extends unknown[]> = (Exp & {
    variables: Vars;
})["expression"];

Я хотел бы добавить индексы Де Брёйна, поскольку это означало бы, что нам больше не нужны интерфейсы, но это потребует некоторой математики кортежей, я думаю, и я пытаюсь этого избежать.

Предложение

Высший порядок, Ламда, Справочные типы

Передайте тип как ссылку

Проще говоря, ссылочный тип или высокого порядка позволил бы отложить параметры, принятые типом, на потом или даже впоследствии вывести параметры типа (обобщения). Но почему нам должно быть до этого дело?

Если мы можем передать тип как ссылку, это означает, что мы можем отложить TypeScript от оценки типа до тех пор, пока мы не решим это сделать. Давайте посмотрим на пример из реального мира:

Предварительный просмотр с Pipe

Представьте, что вы разрабатываете универсальный тип для pipe . Большая часть работы заключается в проверке того, что функции, которые должны быть переданы по конвейеру, действительно поддерживают конвейерную передачу, в противном случае мы вызовем ошибку для пользователя. Для этого мы бы использовали сопоставленный тип для передачи по конвейеру типов функций, таких как pipe(...) :

type  PipeSync<Fns  extends  Function[], K  extends  keyof  Fns> = 
    K  extends  '0'
    // If it's the first function, we leave it unchanged
    ?  Fns[K]
    // For all the other functions, we link input<-output
    : (arg:  Return<Fns[Pos<Prev<IterationOf<K & string>>>]>) =>
        Return<Fns[Pos<IterationOf<K & string>>]>;

Теперь нам просто нужно повторить это для функций с отображаемым типом:

type  Piper<Fns  extends  Function[]> = {
    [K  in  keyof  Fns]:  PipeSync<Fns, K>
}

( см. полную реализацию )

Теперь мы можем объединять функции вместе, и TypeScript может выдавать нам предупреждения:

declare  function  pipe<Fns  extends  F.Function[]>(...args:  F.Piper<Fns>):  F.Pipe<Fns>

const  piped  =  pipe(
    (name:  string, age:  number) => ({name, age}),
    (info: {name:  string, age:  number}) =>  `Welcome, ${info.name}`,
    (message:  object) =>  false, // /!\ ERROR
)

Оно работает! мы получили правильную ошибку:

Аргумент типа '(message: object) => boolean' не может быть назначен параметру типа '(arg: string) => boolean'.

Проблема

Но существует проблема. Хотя это очень хорошо работает для простых операций, вы обнаружите, что это полностью не работает, когда вы начнете использовать универсальные шаблоны (шаблоны) для функций, которые вы ему передаете:

const  piped  =  pipe(
    (a:  string) =>  a,
    <B>(b:  B) =>  b, // any
    <C>(c:  C) =>  c, // any
)

type  piped  =  Piper<[
    (a:  string) =>  string,
    <B>(b:  B) =>  B,
    <C>(c:  C) =>  C,
]>
// [
//     (a:  string) =>  string,
//     (b:  string) =>  unknown,
//     (c:  unknown) => unknown
// ]

В обоих случаях TypeScript потерял из виду типы функций.
> Именно здесь в игру вступают типы высшего порядка <

Синтаксис

type  PipeSync<Fns  extends  Function[], K  extends  keyof  Fns> = 
    K  extends  '0'
    // If it's the first function, we leave it unchanged
+   ?  *(Fns[K]) // this will preserve the generics
    // For all the other functions, we link input<-output
+   :  *( // <- Any type can be made a reference
+       <T>(arg:  T) => Return<*(Fns[Pos<IterationOf<K  &  string>>])>
+       // vvv It is now a reference, we can assign generics
+       )<Return<*(Fns[Pos<Prev<IterationOf<K  &  string>>>])>>
+       // ^^^ We also tell TS not to evaluate the previous return
+       // and this could be achieved by making it a reference too

Короче говоря, мы вручную и динамически вывели дженерики с помощью * . Фактически, использование * отложило оценку дженериков. Таким образом, * ведет себя по-разному в зависимости от контекста. Если * относится к типу, который:

  • может получать дженерики : берет на себя свои дженерики / получает ссылку
  • сам по себе является универсальным : он откладывает оценку до тех пор, пока не станет известна ссылка. Для этого строится ссылочное дерево от родителя (ей) вниз. Другими словами, ссылки могут быть вложенными. Это именно то, что происходит выше с Return<*(Fns[Pos<Prev<IterationOf<K & string>>>])> которому присвоено T . В этом контексте мы можем сказать, что * «защищает» от немедленной оценки.
  • Ничего из вышеперечисленного : ничего не делает, разрешается к тому же типу
type  piped  =  Piper<[
    (a:  string) =>  string,
    <B>(b:  B) =>  B
    <C>(c:  C) =>  C
]>
// [
//     (a:  string) =>  string,
//     (b:  string) =>  string,
//     (c:  string) =>  string
// ]

Таким образом, TypeScript должен начинать / продолжать оценку только в том случае, если был предоставлен универсальный вариант, и при необходимости блокировать оценку (неполный универсальный). На данный момент TS выполняет оценку за один раз, превращая дженерики в типы unknown . С этим предложением, когда что-то не может быть решено:

type  piped  =  Piper<[
    <A>(a:  A) =>  A, // ?
    <B>(b:  B) =>  B, // ?
    <C>(c:  C) =>  C, // ?
]>
// [
//     <A>(a:  A) =>  A,
//     (b:  A) =>  A,
//     (c:  A) =>  A
// ]

Детали

* извлекает ссылку на тип, позволяя манипулировать его обобщениями. Таким образом, размещение подстановочного знака перед типом возвращает ссылку на него:

*[type]

Получение ссылки на тип автоматически включает манипуляции с универсальными шаблонами:

*[type]<T0, T1, T2...>

Дженерики потребляются / устанавливаются только целевым типом, если это возможно. Так поступаем так:

*string<object, null> // Will resolve to `string`

Но он также может быть проверен самим TypeScript, должен ли он отображать предупреждение или нет. Но внутренне TS ничего из этого делать не должен.

Я также подумал, что было бы неплохо использовать * поскольку он может символизировать указатель на что-то (например, в языках C / C ++) и не заимствован TypeScript.

Типы высшего порядка

Теперь, когда мы увидели, как это работает в самой простой форме, я хотел бы представить основную концепцию: лямбда-типы . Было бы неплохо иметь анонимные типы, похожие на обратные вызовы, лямбды, ссылки в JavaScript .

В приведенном выше примере показано, как взять на себя обобщенные функции функции. Но поскольку мы говорим о ссылках, любой тип может использоваться вместе с * . Проще говоря, ссылка на тип - это тип, который мы можем передавать, но который еще не получил своих обобщений:

type  A<T  extends  string> = {0:  T}
type  B<T  extends  string> = [T]
type  C<T  extends  number> = 42

// Here's our lamda
type  Referer<*Ref<T  extends  string>, T  extends  string> =  Ref<T>
// Notice that `T` & `T` are not in conflict
// Because they're bound to their own scopes

type  testA  =  Referer<A, 'hi'> // {0: 'hi'}
type  testB  =  Referer<B, 'hi'> // ['hi']
type  testC  =  Referer<C, 'hi'> // ERROR

Высшие родственные типы

interface Monad<*T<X extends any>> {
  map<A, B>(f: (a: A) => B): T<A> => T<B>;
  lift<A>(a: A): T<A>;
  join<A>(tta: T<T<A>>): T<A>;
}

Условия поиска

выше # порядок # тип # ссылки # лямбда #HKTs

@ pirix-gh, если вы прочитаете всего несколько сообщений, вы увидите, что многое из того, о чем вы просите, либо уже возможно, либо уже было запрошено.

Я читал их, я думал, что смогу обобщить свои идеи, как и все остальные (для решения «все в одном»), в основном о синтаксисе.

Я отредактировал вышеприведенное предложение для лучшего объяснения того, как мы могли бы связать ссылки, и исправил способ работы такого типа, как Pipe (было несколько ошибок относительно его логики).

Есть обновления?

Все еще нет обновления? На мой взгляд, эта проблема является препятствием номер один на пути к полной реализации потенциала TypeScript. Есть так много случаев, когда я пытаюсь правильно набрать свои библиотеки, но сдаюсь после долгой борьбы, понимая, что снова столкнулся с этим ограничением. Это повсеместно, проявляясь даже в, казалось бы, очень простых сценариях. Очень надеюсь, что это скоро будет рассмотрено.

interface Monad<T<X>> {
    map1<A, B>(f: (a: A) => B): (something: A) => B;

    map<A, B>(f: (a: A) => B): (something: T<A>) => T<B>;

    lift<A>(a: A): T<A>;
    join<A>(tta: T<T<A>>): T<A>;
}

type sn = (tmp: string) => number

function MONAD(m: Monad<Set>,f:sn) {
    var w = m.map1(f);    // (method) Monad<Set>.map1<string, number>(f: (a: string) => number): (something: string) => number
    var w2 = m.map(f);    // (method) Monad<Set>.map<string, number>(f: (a: string) => number): (something: Set<string>) => Set<number>
    var q = m.lift(1);    // (method) Monad<Set>.lift<number>(a: number): Set<number>
    var a = new Set<Set<number>>();
    var w = m.join(q);    // (method) Monad<Set>.join<unknown>(tta: Set<Set<unknown>>): Set<unknown>.  You could see that typeParameter infer does not work for now.
    var w1 = m.join<number>(q);    // (method) Monad<Set>.join<number>(tta: Set<Set<number>>): Set<number>
}

Еще предстоит проделать большую работу, например: исправить quickinfo, typeParameter infer, добавить сообщение об ошибке, выделить тот же typeConstructor .....
Но это начинает работать, и вот что я могу пока получить.
Пример интерфейса взят из https://github.com/microsoft/TypeScript/issues/1213#issuecomment -523245130, вывод действительно очень полезен, большое спасибо.

Я надеюсь, что общение может предоставить больше таких пользовательских случаев, чтобы проверить, работает ли текущий способ в большинстве ситуаций.

Также было бы приятно предоставить некоторую информацию о HKT / программировании функций / лямбда (когда я говорю lambda , я имею в виду математику, я мог найти только пример, написанный на каком-то языке, без математики)

Вот вещи, которые мне очень помогают:

@ShuiRuTian Что касается того, что m.join(q) возвращает Set<unknown> , я предполагаю, что --noImplicitAny вызывает также предупреждение?

Я надеюсь, что сообщество сможет предоставить больше таких пользовательских случаев, чтобы проверить, работает ли текущий способ в большинстве ситуаций.

Также было бы приятно предоставить некоторую информацию о HKT / программировании функций / лямбда (когда я говорю lambda , я имею в виду математику, я мог найти только пример, написанный на каком-то языке, без математики)

Не вдаваясь в подробности, я недавно попытался создать общую каррированную функцию filter , и я хотел, чтобы она делала что-то вроде этого:

const filterNumbers = filter(
    (item: number | string): item is number => typeof item === "number"
);

const array = ["foo", 1, 2, "bar"]; // (number | string)[]
const customObject = new CustomObject(); // CustomObject<number | string>

filterNumbers(array); // number[] inferred
filterNumbers(customObjectWithFilterFunction); // CustomObject<number> inferred

И у меня нет способа сделать это, потому что мне нужен способ сообщить TypeScript «вернуть тот же тип, который вы получили, но с этим другим параметром». Что-то вроде этого:

const filter = <Item, FilteredItem>(predicate: (item: Item) => item is FilteredItem) =>
  <Filterable<~>>(source: Filterable<Item>): Filterable<FilteredItem> => source.filter(predicate);

@lukeshiru да, по сути это https://pursuit.purescript.org/packages/purescript-filterable/2.0.1/docs/Data.Filterable#v : filter
Есть много других подобных вариантов использования HKT в TypeScript.

@isiahmeadows Я пробовал. Ты прав.
@lukeshiru и @raveclassic Спасибо! Эта функция кажется вполне разумной. Я бы посмотрел на это после прочтения https://gcanti.github.io/fp-ts/learning-resources/

Я застрял и не знаю, каков текущий « обходной путь» ...
Я пытаюсь реализовать спецификацию цепочки :

m['fantasy-land/chain'](f)

Значение, реализующее спецификацию Chain, также должно реализовывать спецификацию Apply.

a['fantasy-land/ap'](b)

Я сделал FunctorSimplex который затем расширен на FunctorComplex затем расширен на Apply но теперь, когда я хочу расширить Apply как Chain ломается ...

Мне это нужно (изображение ниже и ссылка на код):

(Мне нужно передать тип ApType чтобы в строке 12 Apply не было «жестко запрограммировано», а было общим ... чтобы также принимать любые типы, расширенные от IApply )

Screenshot

Постоянная ссылка на строки фрагмента кода с

Машинопись
тип экспорта ApType = ( ap: Применить <(val: A) => B>, ) => IApply ;

/ * [...] * /

интерфейс экспорта IApply расширяет FunctorComplex {
/ ** Fantasy-land/ap :: Apply f => f a ~> f (a -> b) -> f b * /
ap: ApType ;
}
``

Упоминается в вопросе о переполнении стека: Проблема с универсальными типами TypeScript: требуется обходной путь

@Luxcium До тех высокодородные типы, возможна только их эмуляция. Вы можете посмотреть там, чтобы увидеть, как можно достичь:

Пока TS не поддерживает более высокодородные типы, возможна только их эмуляция. Вы можете посмотреть там, чтобы увидеть, как можно достичь

Много танков @kapke Я, вероятно, слишком pseudoFnAdd(15)(27) // 42 я хотел бы иметь возможность с TypeScript писать pseudoType<someClassOrConstructor><number> // unknown но я скрипач, а не _ ° какой-то человек, который долгое время учился в университете или что-то в этом роде ° _

Эта информация и лекции (чтения) очень ценятся ...

Примечание: я говорю по-французски, по-французски слово _lecture (s) _ имеет значение _readings_, а не «сердитый или серьезный разговор с кем-то с целью критики его поведения» ...

Вероятно, следующее, что я придумал в качестве простого обходного пути без PR, не будет работать во всех случаях, но я думаю, что стоит упомянуть:

type AGenericType<T> = T[];

type Placeholder = {'aUniqueKey': unknown};
type Replace<T, X, Y> = {
  [k in keyof T]: T[k] extends X ? Y : T[k];
};

interface Monad<T> {
  map<A, B>(f: (a: A) => B): (v: Replace<T, Placeholder, A>) => Replace<T, Placeholder, B>;
  lift<A>(a: A): Replace<T, Placeholder, A>;
  join<A>(tta: Replace<T, Placeholder, Replace<T, Placeholder, A>>): Replace<T, Placeholder, A>;
}

function MONAD(m: Monad<AGenericType<Placeholder>>, f: (s: string) => number) {
  var a = m.map(f); // (v: string[]) => number[]
  var b = m.lift(1); // number[]
  var c = m.join([[2], [3]]); // number[]
}
Была ли эта страница полезной?
0 / 5 - 0 рейтинги
bleepcoder.com использует общественно лицензированную информацию GitHub для предоставления решений разработчикам по всему миру. Мы не аффилированы с GitHub, Inc. или любым другим разработчиком, использующим GitHub для своих проектов. Мы не размещаем видео или изображения на наших серверах. Все права принадлежат их соответствующим владельцам.
Источник для этой страницы: Источник

Популярные языки программирования
Популярные проекты GitHub
Больше проектов GitHub

© 2024 bleepcoder.com - Contact
Made with in the Dominican Republic.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.