Repository: marpple/FxTS (ts)

Issue:
Feature request - `curry` utility
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->

`curry` is an essential utility for FP, yet most libraries (especially `lodash`) fail to provide correct type definition.
Are there any plans for exporting `curry`? (since other utilities from `fxts` support currying, as I noticed)

## 💻 Use Cases

<!-- What do you want to use this for? -->

```typescript
const strictEq = curry(<T>(left: T, right: T) => left === right)

const twos = pipe([1, 1, 1, 2, 2, 2], filter(strictEq(2))) // [2, 2, 2]
```


Pull request description:
feat: add `curry` function
Fixes #12 

Patch:
diff --git a/package.json b/package.json
index 3e934d9e..15a19114 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@fxts/core",
   "version": "0.2.3",
-  "description": "",
+  "description": "A functional library for TypeScript/JavaScript programmers.",
   "type": "module",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/idnex.js",
@@ -40,15 +40,19 @@
   },
   "repository": {
     "type": "git",
-    "url": "git+https://github.com/marpple/FxTS.git"
+    "url": "https://github.com/marpple/FxTS.git"
   },
-  "keywords": [],
+  "keywords": [
+    "fp",
+    "lazy",
+    "concurrency"
+  ],
   "author": "",
   "license": "Apache-2.0",
   "bugs": {
     "url": "https://github.com/marpple/FxTS/issues"
   },
-  "homepage": "https://github.com/marpple/FxTS#readme",
+  "homepage": "https://fxts.dev",
   "devDependencies": {
     "@babel/cli": "^7.16.0",
     "@babel/core": "^7.16.0",
diff --git a/src/Lazy/index.ts b/src/Lazy/index.ts
index 1071ec26..994b5738 100644
--- a/src/Lazy/index.ts
+++ b/src/Lazy/index.ts
@@ -23,6 +23,7 @@ import toAsync from "./toAsync";
 import uniq from "./uniq";
 import uniqBy from "./uniqBy";
 import zip from "./zip";
+import zipWith from "./zipWith";
 import zipWithIndex from "./zipWithIndex";
 
 export {
@@ -51,5 +52,6 @@ export {
   uniq,
   uniqBy,
   zip,
+  zipWith,
   zipWithIndex,
 };
diff --git a/src/Lazy/zipWith.ts b/src/Lazy/zipWith.ts
new file mode 100644
index 00000000..a730a0f1
--- /dev/null
+++ b/src/Lazy/zipWith.ts
@@ -0,0 +1,61 @@
+import { map, zip } from ".";
+import { isAsyncIterable, isIterable } from "../_internal/utils";
+
+/**
+ * Returns Iterable/AsyncIterable out of the two supplied by applying `f` to each same positioned pair in Iterable/AsyncIterable.
+ *
+ * @example
+ * const iter = zipWith((a,b) => [a,b], [1,2,3], ['a','b','c']);
+ * iter.next(); // {value: [1, 'a'] , done: false}
+ * iter.next(); // {value: [2, 'b'] , done: false}
+ * iter.next(); // {value: [3, 'c'] , done: false}
+ * iter.next(); // {value: undefined , done: true}
+ */
+function zipWith<A, B, C>(
+  f: (a: A, b: B) => C,
+  iterable1: Iterable<A>,
+  iterable2: Iterable<B>,
+): IterableIterator<C>;
+
+function zipWith<A, B, C>(
+  f: (a: A, b: B) => C,
+  iterable1: Iterable<A>,
+  iterable2: AsyncIterable<B>,
+): AsyncIterableIterator<C>;
+
+function zipWith<A, B, C>(
+  f: (a: A, b: B) => C,
+  iterable1: AsyncIterable<A>,
+  iterable2: Iterable<B>,
+): AsyncIterableIterator<C>;
+
+function zipWith<A, B, C>(
+  f: (a: A, b: B) => C,
+  iterable1: AsyncIterable<A>,
+  iterable2: AsyncIterable<B>,
+): AsyncIterableIterator<C>;
+
+function zipWith<A, B, C>(
+  f: (a: A, b: B) => C,
+  iterable1: Iterable<A> | AsyncIterable<A>,
+  iterable2: Iterable<B> | AsyncIterable<B>,
+): IterableIterator<C> | AsyncIterableIterator<C> {
+  if (isIterable(iterable1) && isIterable(iterable2)) {
+    return map(([a, b]) => f(a, b), zip(iterable1, iterable2));
+  }
+  if (isIterable(iterable1) && isAsyncIterable(iterable2)) {
+    return map(([a, b]) => f(a, b), zip(iterable1, iterable2));
+  }
+  if (isAsyncIterable(iterable1) && isIterable(iterable2)) {
+    return map(([a, b]) => f(a, b), zip(iterable1, iterable2));
+  }
+  if (isAsyncIterable(iterable1) && isAsyncIterable(iterable2)) {
+    return map(([a, b]) => f(a, b), zip(iterable1, iterable2));
+  }
+
+  throw new TypeError(
+    "'iterable1' and 'iterable2' must be type of Iterable or AsyncIterable",
+  );
+}
+
+export default zipWith;
diff --git a/src/curry.ts b/src/curry.ts
new file mode 100644
index 00000000..1dfc8323
--- /dev/null
+++ b/src/curry.ts
@@ -0,0 +1,32 @@
+import Curry from "./types/Curry";
+
+/**
+ * Returns a curried function of `f`
+ *
+ * @example
+ * ```ts
+ * const add = (a: number, b: number): number => a + b
+ *
+ * const curried = curry(add)
+ * const add10 = curried(10)
+ * console.log(add10(5)) // 15
+ * console.log(curried(3, 4)) // 7
+ * ```
+ */
+
+function curry<F extends (...args: any[]) => any>(f: F): Curry<F> {
+  const arity = f.length;
+
+  return (function resolver(...args: unknown[]) {
+    const memory = [...args];
+
+    return function (...innerArgs: unknown[]) {
+      const local = [...memory, ...innerArgs];
+      const next = local.length >= arity ? f : resolver;
+
+      return next(...local);
+    };
+  })() as any;
+}
+
+export default curry;
diff --git a/src/index.ts b/src/index.ts
index f0f62157..59672750 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,5 @@
 import countBy from "./countBy";
+import curry from "./curry";
 import delay from "./delay";
 import each from "./each";
 import every from "./every";
@@ -24,6 +25,7 @@ import toArray from "./toArray";
 
 export {
   countBy,
+  curry,
   delay,
   each,
   every,
diff --git a/src/types/Curry.ts b/src/types/Curry.ts
new file mode 100644
index 00000000..8c55fc47
--- /dev/null
+++ b/src/types/Curry.ts
@@ -0,0 +1,19 @@
+import Length from "./Length";
+import Cast from "./Cast";
+import Drop from "./Drop";
+
+type CleanGaps<O extends any[]> = {
+  [K in keyof O]: NonNullable<O[K]>;
+};
+
+type Gaps<L extends any[]> = Cast<CleanGaps<{ [K in keyof L]?: L[K] }>, any[]>;
+
+type Curry<F extends (...args: any[]) => any> = <
+  T extends any[],
+  G = Drop<Length<T>, Parameters<F>>,
+  R = ReturnType<F>,
+>(
+  ...args: Cast<T, Gaps<Parameters<F>>>
+) => G extends [any, ...any[]] ? Curry<(...args: G) => R> : R;
+
+export default Curry;
diff --git a/src/types/Prepend.ts b/src/types/Prepend.ts
index a36b248f..bacb9ef1 100644
--- a/src/types/Prepend.ts
+++ b/src/types/Prepend.ts
@@ -1,3 +1,3 @@
-type Prepend<ARR extends any[], T> = [T, ...ARR];
+type Prepend<ARR extends unknown[], T> = [T, ...ARR];
 
 export default Prepend;


Test patch:
diff --git a/test/Lazy/zipWith.spec.ts b/test/Lazy/zipWith.spec.ts
new file mode 100644
index 00000000..726de3a1
--- /dev/null
+++ b/test/Lazy/zipWith.spec.ts
@@ -0,0 +1,57 @@
+import { toAsync } from "../../src";
+import zipWith from "../../src/Lazy/zipWith";
+
+describe("zipWith", function () {
+  const iter1 = ["foo", "bar", "ha"];
+  const iter2 = [1, 2, 3];
+  const then = [{ foo: 1 }, { bar: 2 }, { ha: 3 }];
+
+  describe("sync", function () {
+    it("should apply `f` to each same positioned pair", function () {
+      const acc = [];
+      const iter = zipWith((a, b) => ({ [a]: b }), iter1, iter2);
+      for (const a of iter) {
+        acc.push(a);
+      }
+      expect(acc).toEqual(then);
+    });
+  });
+
+  describe("async", function () {
+    it("should apply `f` to each same positioned pair [iterable/AsyncIterable]", async function () {
+      const acc = [];
+      const iter = zipWith((a, b) => ({ [a]: b }), toAsync(iter1), iter2);
+      for await (const a of iter) {
+        acc.push(a);
+      }
+
+      expect(acc).toEqual(then);
+    });
+
+    it("should apply `f` to each same positioned pair [AsyncIterable/iterable]", async function () {
+      const acc = [];
+      const iter = zipWith((a, b) => ({ [a]: b }), toAsync(iter1), iter2);
+
+      for await (const a of iter) {
+        acc.push(a);
+      }
+
+      expect(acc).toEqual(then);
+    });
+
+    it("should apply `f` to each same positioned pair [AsyncIterable/AsyncIterable]", async function () {
+      const acc = [];
+      const iter = zipWith(
+        (a, b) => ({ [a]: b }),
+        toAsync(iter1),
+        toAsync(iter2),
+      );
+
+      for await (const a of iter) {
+        acc.push(a);
+      }
+
+      expect(acc).toEqual(then);
+    });
+  });
+});
diff --git a/test/curry.spec.ts b/test/curry.spec.ts
new file mode 100644
index 00000000..3e4578d3
--- /dev/null
+++ b/test/curry.spec.ts
@@ -0,0 +1,20 @@
+import { curry } from "../src";
+
+describe("curry", function () {
+  it("should not change a unary function", function () {
+    const unary = (a: string) => a + "bar";
+    const curried = curry(unary);
+    expect(curried("foo")).toBe("foobar");
+  });
+
+  it("given function should be curried", function () {
+    const add = (a: number, b: number, c: number) => a + b + c;
+    const curried = curry(add);
+    expect(curried(1)(2)(3)).toBe(6);
+    expect(curried(1, 2)(3)).toBe(6);
+    expect(curried(1)(2, 3)).toBe(6);
+    expect(curried(1, 2, 3)).toBe(6);
+    expect(curried()(1, 2, 3)).toBe(6);
+    expect(curried(1)()(2, 3)).toBe(6);
+  });
+});
diff --git a/type-check/Lazy/zipWith.test.ts b/type-check/Lazy/zipWith.test.ts
new file mode 100644
index 00000000..d19235a0
--- /dev/null
+++ b/type-check/Lazy/zipWith.test.ts
@@ -0,0 +1,47 @@
+import * as Test from "../../src/types/Test";
+import { toAsync, zipWith } from "../../src";
+
+const { checks, check } = Test;
+
+const iter1 = ["foo", "bar", "ha"];
+const iter2 = [1, 2, 3];
+
+const res1 = zipWith((a, b) => ({ [a]: b }), iter1, iter2);
+const res2 = zipWith((a, b) => ({ [a]: b }), toAsync(iter1), iter2);
+const res3 = zipWith((a, b) => ({ [a]: b }), iter1, toAsync(iter2));
+const res4 = zipWith((a, b) => ({ [a]: b }), toAsync(iter1), toAsync(iter2));
+const res5 = zipWith((a, b) => [a, b], iter1, iter2);
+const res6 = zipWith((a, b) => [a, b] as [string, number], iter1, iter2);
+
+checks([
+  check<
+    typeof res1,
+    IterableIterator<{
+      [x: string]: number;
+    }>,
+    Test.Pass
+  >(),
+  check<
+    typeof res2,
+    AsyncIterableIterator<{
+      [x: string]: number;
+    }>,
+    Test.Pass
+  >(),
+  check<
+    typeof res3,
+    AsyncIterableIterator<{
+      [x: string]: number;
+    }>,
+    Test.Pass
+  >(),
+  check<
+    typeof res4,
+    AsyncIterableIterator<{
+      [x: string]: number;
+    }>,
+    Test.Pass
+  >(),
+  check<typeof res5, IterableIterator<(string | number)[]>, Test.Pass>(),
+  check<typeof res6, IterableIterator<[string, number]>, Test.Pass>(),
+]);
diff --git a/type-check/curry.test.ts b/type-check/curry.test.ts
new file mode 100644
index 00000000..3fac72c9
--- /dev/null
+++ b/type-check/curry.test.ts
@@ -0,0 +1,37 @@
+import * as Test from "../src/types/Test";
+import Curry from "../src/types/Curry";
+import { curry, pipe, toAsync } from "../src";
+import { filter } from "../src/Lazy";
+
+const { checks, check } = Test;
+
+declare function fn(a: string, c: boolean, ...d: number[]): string;
+
+declare const curried: Curry<typeof fn>;
+
+const res1 = curried("a");
+const res2 = res1();
+const res3 = res2(true);
+const res4 = res2(false, 1, 2, 3);
+
+const strictEq = curry(<T>(left: T, right: T) => left === right);
+
+const res5 = pipe([1, 1, 1, 2, 2, 2], filter(strictEq(2)));
+const res6 = pipe([1, 1, 1, 2, 2, 2], toAsync, filter(strictEq(2)));
+
+checks([
+  check<
+    typeof res1,
+    Curry<(c: boolean, ...d: number[]) => string>,
+    Test.Pass
+  >(),
+  check<
+    typeof res2,
+    Curry<(c: boolean, ...d: number[]) => string>,
+    Test.Pass
+  >(),
+  check<typeof res3, string, Test.Pass>(),
+  check<typeof res4, string, Test.Pass>(),
+  check<typeof res5, IterableIterator<number>, Test.Pass>(),
+  check<typeof res6, AsyncIterableIterator<number>, Test.Pass>(),
+]);
diff --git a/type-check/types/Length.test.ts b/type-check/types/Length.test.ts
index 0663f2f0..e258541a 100644
--- a/type-check/types/Length.test.ts
+++ b/type-check/types/Length.test.ts
@@ -6,4 +6,5 @@ const { checks, check } = Test;
 checks([
   check<Length<[]>, 0, Test.Pass>(),
   check<Length<[1, 2, 3, 4]>, 4, Test.Pass>(),
+  check<Length<[1, 2, 3, 4, ...number[]]>, number, Test.Pass>(),
 ]);
diff --git a/type-check/types/Prepend.test.ts b/type-check/types/Prepend.test.ts
new file mode 100644
index 00000000..10502726
--- /dev/null
+++ b/type-check/types/Prepend.test.ts
@@ -0,0 +1,9 @@
+import * as Test from "../../src/types/Test";
+import Prepend from "../../src/types/Prepend";
+
+const { checks, check } = Test;
+
+checks([
+  check<Prepend<[], number>, [number], Test.Pass>(),
+  check<Prepend<[1, true], string>, [string, 1, true], Test.Pass>(),
+]);