Skip to content

Commit

Permalink
more type safe dual signature, closes #2967
Browse files Browse the repository at this point in the history
  • Loading branch information
kristiannotari authored and tim-smart committed Jul 4, 2024
1 parent 6c77633 commit f933411
Show file tree
Hide file tree
Showing 14 changed files with 147 additions and 43 deletions.
8 changes: 8 additions & 0 deletions .changeset/nice-laws-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@effect/typeclass": minor
"@effect/platform": minor
"effect": minor
"@effect/schema": minor
---

more type safe `Function.dual` signature
24 changes: 24 additions & 0 deletions packages/effect/dtslint/Function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as Function from "effect/Function"

// -------------------------------------------------------------------------------------
// dual
// -------------------------------------------------------------------------------------

type f_data_first = (a: number, b: number) => number
type f_data_last = (b: number) => (a: number) => number

const f: f_data_first & f_data_last = Function.dual(2, (a: number, b: number) => a + b)
// $ExpectType f_data_first & f_data_last
f

const f_explicit_types = Function.dual<f_data_last, f_data_first>(2, (a: number, b: number) => a + b)
// $ExpectType f_data_first & f_data_last
f_explicit_types

const f_no_type = Function.dual(2, (a: number, b: number) => a + b)
// $ExpectType (a: number, b: number) => number
f_no_type

// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const f_wrong_impl: f_data_first & f_data_last = Function.dual(2, (a: string, b: string) => a + b)
9 changes: 6 additions & 3 deletions packages/effect/src/Array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -994,9 +994,12 @@ export const findLast: {
<A, B>(self: Iterable<A>, f: (a: A, i: number) => Option<B>): Option<B>
<A, B extends A>(self: Iterable<A>, refinement: (a: A, i: number) => a is B): Option<B>
<A>(self: Iterable<A>, predicate: (a: A, i: number) => boolean): Option<A>
} = dual(
} = dual<
typeof findLast,
<A>(self: Iterable<A>, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option<A>)) => Option<A>
>(
2,
<A>(self: Iterable<A>, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option<A>)): Option<A> => {
(self, f) => {
const input = fromIterable(self)
for (let i = input.length - 1; i >= 0; i--) {
const a = input[i]
Expand Down Expand Up @@ -1680,7 +1683,7 @@ export const splitNonEmptyAt: {
export const split: {
(n: number): <A>(self: Iterable<A>) => Array<Array<A>>
<A>(self: Iterable<A>, n: number): Array<Array<A>>
} = dual(2, <A>(self: Iterable<A>, n: number) => {
} = dual<typeof split, <A>(self: Iterable<A>, n: number) => Array<Array<A>>>(2, (self, n) => {
const input = fromIterable(self)
return chunksOf(input, Math.ceil(input.length / Math.floor(n)))
})
Expand Down
22 changes: 15 additions & 7 deletions packages/effect/src/Function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,22 @@ export const isFunction = (input: unknown): input is Function => typeof input ==
* @since 2.0.0
*/
export const dual: {
<DataLast extends (...args: Array<any>) => any, DataFirst extends (...args: Array<any>) => any>(
arity: Parameters<DataFirst>["length"],
body: DataFirst
): DataLast & DataFirst
<DataLast extends (...args: Array<any>) => any, DataFirst extends (...args: Array<any>) => any>(
<
Other extends (...args: ReadonlyArray<any>) => any,
Impl extends (...args: ReadonlyArray<any>) => any,
Signature extends Impl = Impl & ([Array<any>] extends [Parameters<Other>] ? unknown : Other)
>(
arity: Parameters<Impl>["length"],
body: Impl
): Signature
<
Other extends (...args: ReadonlyArray<any>) => any,
Impl extends (...args: ReadonlyArray<any>) => any,
Signature extends Impl = Impl & ([Array<any>] extends [Parameters<Other>] ? unknown : Other)
>(
isDataFirst: (args: IArguments) => boolean,
body: DataFirst
): DataLast & DataFirst
body: Impl
): Signature
} = function(arity, body) {
if (typeof arity === "function") {
return function() {
Expand Down
17 changes: 13 additions & 4 deletions packages/effect/src/Iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,12 @@ export const findFirst: {
<A, B>(self: Iterable<A>, f: (a: A, i: number) => Option<B>): Option<B>
<A, B extends A>(self: Iterable<A>, refinement: (a: A, i: number) => a is B): Option<B>
<A>(self: Iterable<A>, predicate: (a: A, i: number) => boolean): Option<A>
} = dual(
} = dual<
typeof findFirst,
<A>(self: Iterable<A>, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option<A>)) => Option<A>
>(
2,
<A>(self: Iterable<A>, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option<A>)): Option<A> => {
(self, f) => {
let i = 0
for (const a of self) {
const o = f(a, i)
Expand Down Expand Up @@ -410,7 +413,10 @@ export const findLast: {
<A, B>(self: Iterable<A>, f: (a: A, i: number) => Option<B>): Option<B>
<A, B extends A>(self: Iterable<A>, refinement: (a: A, i: number) => a is B): Option<B>
<A>(self: Iterable<A>, predicate: (a: A, i: number) => boolean): Option<A>
} = dual(
} = dual<
typeof findLast,
<A>(self: Iterable<A>, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option<A>)) => Option<A>
>(
2,
<A>(self: Iterable<A>, f: ((a: A, i: number) => boolean) | ((a: A, i: number) => Option<A>)): Option<A> => {
let i = 0
Expand Down Expand Up @@ -795,7 +801,10 @@ export const filterMap: {
export const filterMapWhile: {
<A, B>(f: (a: A, i: number) => Option<B>): (self: Iterable<A>) => Iterable<B>
<A, B>(self: Iterable<A>, f: (a: A, i: number) => Option<B>): Iterable<B>
} = dual(2, <A, B>(self: Iterable<A>, f: (a: A, i: number) => Option<B>) => ({
} = dual<
typeof filterMapWhile,
<A, B>(self: Iterable<A>, f: (a: A, i: number) => Option<B>) => Iterable<B>
>(2, (self, f) => ({
[Symbol.iterator]() {
const iterator = self[Symbol.iterator]()
let i = 0
Expand Down
2 changes: 1 addition & 1 deletion packages/effect/src/Option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ export const map: {
*/
export const as: {
<B>(b: B): <X>(self: Option<X>) => Option<B>
} = dual(2, <X, B>(self: Option<X>, b: B): Option<B> => map(self, () => b))
} = dual<typeof as, <X, B>(self: Option<X>, b: B) => Option<B>>(2, (self, b) => map(self, () => b))

/**
* Maps the `Some` value of this `Option` to the `void` constant value.
Expand Down
8 changes: 5 additions & 3 deletions packages/effect/src/internal/cause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,10 +495,12 @@ export const andThen: {
<E2>(f: Cause.Cause<E2>): <E>(self: Cause.Cause<E>) => Cause.Cause<E2>
<E, E2>(self: Cause.Cause<E>, f: (e: E) => Cause.Cause<E2>): Cause.Cause<E2>
<E, E2>(self: Cause.Cause<E>, f: Cause.Cause<E2>): Cause.Cause<E2>
} = dual(
} = dual<
typeof andThen,
<E, E2>(self: Cause.Cause<E>, f: ((e: E) => Cause.Cause<E2>) | Cause.Cause<E2>) => Cause.Cause<E2>
>(
2,
<E, E2>(self: Cause.Cause<E>, f: ((e: E) => Cause.Cause<E2>) | Cause.Cause<E2>): Cause.Cause<E2> =>
isFunction(f) ? flatMap(self, f) : flatMap(self, () => f)
(self, f) => isFunction(f) ? flatMap(self, f) : flatMap(self, () => f)
)

// -----------------------------------------------------------------------------
Expand Down
13 changes: 12 additions & 1 deletion packages/effect/src/internal/fiberRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1986,7 +1986,18 @@ export const forEach: {
readonly discard: true
}
): Effect.Effect<void, E, R>
} = dual((args) => Predicate.isIterable(args[0]), <A, R, E, B>(
} = dual<
typeof forEach,
<A, R, E, B>(
self: Iterable<A>,
f: (a: A, i: number) => Effect.Effect<B, E, R>,
options?: {
readonly concurrency?: Concurrency | undefined
readonly batching?: boolean | "inherit" | undefined
readonly discard?: boolean | undefined
}
) => Effect.Effect<void, E, R> | Effect.Effect<RA.NonEmptyArray<B>, E, R> | Effect.Effect<Array<B>, E, R>
>((args) => Predicate.isIterable(args[0]), <A, R, E, B>(
self: Iterable<A>,
f: (a: A, i: number) => Effect.Effect<B, E, R>,
options?: {
Expand Down
25 changes: 14 additions & 11 deletions packages/effect/src/internal/hashSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,18 +280,21 @@ export const filter: {
<A>(predicate: Predicate<NoInfer<A>>): (self: HS.HashSet<A>) => HS.HashSet<A>
<A, B extends A>(self: HS.HashSet<A>, refinement: Refinement<A, B>): HS.HashSet<B>
<A>(self: HS.HashSet<A>, predicate: Predicate<A>): HS.HashSet<A>
} = dual(2, <A>(self: HS.HashSet<A>, f: Predicate<A>) => {
return mutate(empty(), (set) => {
const iterator = values(self)
let next: IteratorResult<A, any>
while (!(next = iterator.next()).done) {
const value = next.value
if (f(value)) {
add(set, value)
} = dual<typeof filter, <A>(self: HS.HashSet<A>, f: Predicate<A>) => HS.HashSet<A>>(
2,
<A>(self: HS.HashSet<A>, f: Predicate<A>): HS.HashSet<A> => {
return mutate(empty(), (set) => {
const iterator = values(self)
let next: IteratorResult<A, any>
while (!(next = iterator.next()).done) {
const value = next.value
if (f(value)) {
add(set, value)
}
}
}
})
})
})
}
)

/** @internal */
export const partition: {
Expand Down
11 changes: 10 additions & 1 deletion packages/effect/src/internal/stm/tMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,16 @@ export const removeIf: {
readonly discard: false
}
): STM.STM<Array<[K, V]>>
} = dual((args) => isTMap(args[0]), <K, V>(
} = dual<
typeof removeIf,
<K, V>(
self: TMap.TMap<K, V>,
predicate: (key: K, value: V) => boolean,
options?: {
readonly discard: boolean
}
) => STM.STM<Array<[K, V]> | void>
>((args) => isTMap(args[0]), <K, V>(
self: TMap.TMap<K, V>,
predicate: (key: K, value: V) => boolean,
options?: {
Expand Down
20 changes: 16 additions & 4 deletions packages/effect/test/Function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe("Function", () => {
deepStrictEqual(f(3, 2), 1)
deepStrictEqual(Function.pipe(3, f(2)), 1)
// should ignore excess arguments
deepStrictEqual(f.apply(null, [3, 2, 100] as any), 1)
deepStrictEqual((f as (self: number, that: number) => number).apply(null, [3, 2, 100] as any), 1)
})

it("arity: 0", () => {
Expand Down Expand Up @@ -169,7 +169,7 @@ describe("Function", () => {
deepStrictEqual(f(3, 2), 1)
deepStrictEqual(Function.pipe(3, f(2)), 1)
// should ignore excess arguments
deepStrictEqual(f.apply(null, [3, 2, 100] as any), 1)
deepStrictEqual((f as (self: number, that: number) => number).apply(null, [3, 2, 100] as any), 1)
})

it("arity: 5", () => {
Expand All @@ -180,7 +180,13 @@ describe("Function", () => {
deepStrictEqual(f("_", "a", "b", "c", "d"), "_abcd")
deepStrictEqual(Function.pipe("_", f("a", "b", "c", "d")), "_abcd")
// should ignore excess arguments
deepStrictEqual(f.apply(null, ["_", "a", "b", "c", "d", "e"] as any), "_abcd")
deepStrictEqual(
(f as ((self: string, a: string, b: string, c: string, d: string) => string)).apply(
null,
["_", "a", "b", "c", "d", "e"] as any
),
"_abcd"
)
})

it("arity > 5", () => {
Expand All @@ -191,7 +197,13 @@ describe("Function", () => {
deepStrictEqual(f("_", "a", "b", "c", "d", "e"), "_abcde")
deepStrictEqual(Function.pipe("_", f("a", "b", "c", "d", "e")), "_abcde")
// should ignore excess arguments
deepStrictEqual(f.apply(null, ["_", "a", "b", "c", "d", "e", "f"] as any), "_abcde")
deepStrictEqual(
(f as (self: string, a: string, b: string, c: string, d: string, e: string) => string).apply(
null,
["_", "a", "b", "c", "d", "e", "f"] as any
),
"_abcde"
)
})
})
})
12 changes: 9 additions & 3 deletions packages/platform/src/Transferable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,15 @@ export const schema: {
self: Schema.Schema<A, I, R>,
f: (_: I) => Iterable<globalThis.Transferable>
): Schema.Schema<A, I, R>
} = dual(2, <A, I, R>(
self: Schema.Schema<A, I, R>,
f: (_: I) => Iterable<globalThis.Transferable>
} = dual<
typeof schema,
<A, I, R>(
self: Schema.Schema<A, I, R>,
f: (_: I) => Iterable<globalThis.Transferable>
) => Schema.Schema<A, I, R>
>(2, (
self,
f
) =>
Schema.transformOrFail(
Schema.encodedSchema(self),
Expand Down
14 changes: 10 additions & 4 deletions packages/schema/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2246,12 +2246,15 @@ export const optional: {
self: Schema.All extends S ? "you can't apply optional implicitly, use optional() instead" : S,
options?: Options
): [undefined] extends [Options] ? optional<S> : optionalWithOptions<S, Options>
} = dual((args) => isSchema(args[0]), (from, options) => {
} = (dual((args) => isSchema(args[0]), (
from,
options
) => {
// Note: `Schema.All extends S ? "you can't...` is used to prevent the case where `optional` is implicitly applied.
// For example: `S.String.pipe(S.optional)` would result in `S.String` being inferred as `Schema.All`,
// which is not the intended behavior. This is mostly an aesthetic consideration, so if it causes issues, we can remove it.
return new PropertySignatureWithFromImpl(optionalPropertySignatureAST(from, options), from)
})
})) as unknown as typeof optional

/**
* @since 0.67.0
Expand Down Expand Up @@ -3073,9 +3076,12 @@ export interface extend<Self extends Schema.Any, That extends Schema.Any> extend
export const extend: {
<That extends Schema.Any>(that: That): <Self extends Schema.Any>(self: Self) => extend<Self, That>
<Self extends Schema.Any, That extends Schema.Any>(self: Self, that: That): extend<Self, That>
} = dual(
} = dual<
typeof extend,
<Self extends Schema.Any, That extends Schema.Any>(self: Self, that: That) => extend<Self, That>
>(
2,
<Self extends Schema.Any, That extends Schema.Any>(self: Self, that: That) => make(extendAST(self.ast, that.ast, []))
(self, that) => make(extendAST(self.ast, that.ast, []))
)

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/typeclass/src/Filterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ export const filter: <F extends TypeLambda>(
predicate: (a: A) => boolean
): Kind<F, R, O, E, B>
} = <F extends TypeLambda>(Filterable: Filterable<F>) =>
dual(
dual<
ReturnType<typeof filter<F>>,
<R, O, E, A>(self: Kind<F, R, O, E, A>, predicate: (a: A) => boolean) => Kind<F, R, O, E, A>
>(
2,
<R, O, E, A>(self: Kind<F, R, O, E, A>, predicate: (a: A) => boolean): Kind<F, R, O, E, A> =>
Filterable.filterMap(self, (b) => (predicate(b) ? Option.some(b) : Option.none()))
Expand Down

0 comments on commit f933411

Please sign in to comment.