Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

try new core idea #104

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

try new core idea #104

wants to merge 8 commits into from

Conversation

gvergnaud
Copy link
Owner

@gvergnaud gvergnaud commented Jul 15, 2023

Here is a POC for a slightly different take on hkts. Here are the main ideas

  • Function application is always done through $. This avoids the uncanny valley of having both Fn<...> and sometimes with $<Fn, ...>.
  • $ can take many arguments and supports partial application: $<Add, 1>, $<Add, 1, 2>, $<$<Add, _, 2>, 2> and $<$<$<Add, _, 2>, _>, 3> all work.
  • There is no difference in syntax between partially applying arguments and getting the final result of a function. Today in HOTScript, you have to call $<Fn> on any function if you want the result, even if it's already fully applied.
  • When creating custom functions, you must provide type arguments constraints with Fn<[arg0, arg1, ...]>
  • Function application with $ is type safe. You can't provide a type that isn't assignable to the constraint.

Here is what using it feels like:

import { $, Arg0, Arg1, Args, Fn } from 'hot2';

type ExpectNumber<a extends number> = [a];
// arguments are typed internally:
interface TakeNumAndStr extends Fn<[number, string], boolean> {
  works: ExpectNumber<Arg0<this>>; // ✅
  fails: ExpectNumber<Arg1<this>>;
  //                  ~~~~~~~~~~  ❌
  return: true;
}

interface Div extends Fn<[number, number], number> {
  return: NumberImpl.Div<Arg0<this>, Arg1<this>>;
}

/**
 * Full application
 */

type x = $<Div, 10, 2>; // 5
//   ^?
type y = $<Div, "10", 2>;
//              ~~~ ❌
type z = $<Div, 11, "2">;
//                  ~~~ ❌

/**
 * Partial application in order
 */

type Div1 = $<Div, 10>;
type Three = $<Div1, 2>;
//    ^?
type w = $<$<Div, 10>, "2">;
//                     ~~~ ❌

/**
 * Partial application different order
 */
type DivBy2 = $<Div, _, 2>;
//   ^?
type q = $<DivBy2, 10>; // 5 ✅
//   ^?
type r = $<$<Div, _>, 10, 5>; // ✅
//   ^?

type TakeStr = $<TakeNumAndStr, 10>;
//   ^? Ap<TakeNumAndStr, [10]>
type e = $<TakeStr, 10>;
//                  ~~ ❌

type TakeNum = $<TakeNumAndStr, _, "10">;
//   ^?Ap<TakeNumAndStr, [_, "10"]>
type s = $<TakeNum, 10>;
//                  ~~ FIXME

/**
 * Higher order
 */

interface Map extends Fn<[Fn, any[]]> {
  return: Args<this> extends [infer fn extends Fn, infer tuple]
    ? { [key in keyof tuple]: $<fn, tuple[key]> }
    : never;
}

type z2 = $<Map, $<Div, _, 2>, [2, 4, 6, 8, 10]>;
//   ^? [1, 2, 3, 4, 5]

interface Add extends Fn<[number, number], number> {
  return: NumberImpl.Add<Arg0<this>, Arg1<this>>;
}

interface Mul extends Fn<[number, number], number> {
  return: NumberImpl.Mul<Arg0<this>, Arg1<this>>;
}

type ReduceImpl<fn extends Fn, acc, xs> = xs extends [
  infer first,
  ...infer rest
]
  ? ReduceImpl<fn, $<fn, acc, first>, rest>
  : acc;

interface Reduce<A = any, B = any> extends Fn<[Fn<[B, A], B>, B, A[]], B> {
  return: Args<this> extends [infer fn extends Fn, infer acc, infer tuple]
    ? ReduceImpl<fn, acc, tuple>
    : never;
}

type reduced1 = $<Reduce<number, number>, Add, 0, [2, 4, 6, 8, 10]>;
//   ^? 30
type reduced2 = $<Reduce<number, number>, Mul, 1, [2, 4, 6, 8, 10]>;
//   ^? 3840
type reduced3 = $<Reduce<number, number>, Mul, 1, ["2", "4", "6", "8", "10"]>;
//                                                ~~~~~~~~~~~~~~~~~~~~~~~~~~ ❌
type reducedOops = $<Reduce, Mul, 1, "oops">;
//                                   ~~~~~~ ❌

interface NumToStringReducer extends Fn<[string, number], string> {
  return: `${Arg0<this>}${Arg1<this>}`;
}

interface StringToNumReducer extends Fn<[number, string], number> {
  return: NumberImpl.Add<Arg0<this>, StringImpl.Length<Arg1<this>>>;
}

// prettier-ignore
type reduced4 = $<Reduce<string, number>, StringToNumReducer, 1, ["a", "aa", "aaa", "aaaa", "aaaaa"]>;
//     ^? 16

// prettier-ignore
type reduced5 = $<Reduce<string, number>, NumToStringReducer, 1, ["a", "aa", "aaa", "aaaa", "aaaaa"]>;
//                                        ~~~~~~~~~~~~~~~~~~ ❌

interface ToString extends Fn<[number], string> {
  return: `${Arg0<this>}`;
}

interface ToNumber extends Fn<[string], number> {
  return: Arg0<this> extends `${infer N extends number}` ? N : never;
}

interface Prepend extends Fn<[string, string], string> {
  return: this["args"] extends [
    infer first extends string,
    infer str extends string
  ]
    ? `${first}${str}`
    : never;
}

type Times10<T extends number> = $<ToNumber, $<Prepend, "1", $<ToString, T>>>;

type WrongComposition1<T extends string> = $<Prepend, "1", $<ToNumber, T>>;
//                                                         ~~~~~~~~~~~~~~ ❌
type WrongComposition2<T extends number> = $<Add, 1, $<ToString, T>>;
//                                                   ~~~~~~~~~~~~~~ ❌

type test1 = Times10<10>;
//    ^? 110

@gvergnaud gvergnaud force-pushed the gvergnaud/try-new-core-idea branch from 5cb761c to 2adf1cc Compare July 16, 2023 17:40
@geoffreytools
Copy link

I notice that your interface and functionality is becoming more and more similar to free-types although the underlying implementation is very different. I am considering abandoning this project considering how well you are doing. I talk about a few challenges of implementing type constraints in length in my guide if you want to skim through it.

I wonder if we hit the same design limitations. How does this behave regarding variance in higher order scenarios for example? I also found conditional types to make TS detect excessively deep type instantiations in some scenarios where they are used to defuse type constraints, making it harder to compose types, which is why I default to intersection and provide different helpers for different situations.

folks at https://github.com/Effect-TS might be interested by an implementation enabling specifying variance. At current they use positional arguments with a specific variance for each, which is appropriate for their needs but makes the type lambdas awkward to use and read for the few power users that would want to define theirs. If the community could settle on one interface that is type safe and general that would be awesome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants