Skip to content

Commit

Permalink
Preserve function length property in Effect.fn / `Effect.fnUntrac… (
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Feb 14, 2025
1 parent c407726 commit f70a65a
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 17 deletions.
47 changes: 47 additions & 0 deletions .changeset/nice-numbers-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
"effect": patch
---

Preserve function `length` property in `Effect.fn` / `Effect.fnUntraced`, closes #4435

Previously, functions created with `Effect.fn` and `Effect.fnUntraced` always had a `.length` of `0`, regardless of their actual number of parameters. This has been fixed so that the `length` property correctly reflects the expected number of arguments.

**Before**

```ts
import { Effect } from "effect"

const fn1 = Effect.fn("fn1")(function* (n: number) {
return n
})

console.log(fn1.length)
// Output: 0 ❌ (incorrect)

const fn2 = Effect.fnUntraced(function* (n: number) {
return n
})

console.log(fn2.length)
// Output: 0 ❌ (incorrect)
```

**After**

```ts
import { Effect } from "effect"

const fn1 = Effect.fn("fn1")(function* (n: number) {
return n
})

console.log(fn1.length)
// Output: 1 ✅ (correct)

const fn2 = Effect.fnUntraced(function* (n: number) {
return n
})

console.log(fn2.length)
// Output: 1 ✅ (correct)
```
41 changes: 25 additions & 16 deletions packages/effect/src/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13742,7 +13742,7 @@ export const fn:
const errorDef = new Error()
Error.stackTraceLimit = limit
if (typeof nameOrBody !== "string") {
return function(this: any, ...args: Array<any>) {
return defineLength(nameOrBody.length, function(this: any, ...args: Array<any>) {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const errorCall = new Error()
Expand All @@ -13759,12 +13759,12 @@ export const fn:
errorDef,
errorCall
})
} as any
}) as any
}
const name = nameOrBody
const options = pipeables[0]
return (body: Function, ...pipeables: Array<any>) => {
return function(this: any, ...args: Array<any>) {
return (body: Function, ...pipeables: Array<any>) =>
defineLength(body.length, function(this: any, ...args: Array<any>) {
const limit = Error.stackTraceLimit
Error.stackTraceLimit = 2
const errorCall = new Error()
Expand All @@ -13779,10 +13779,16 @@ export const fn:
errorDef,
errorCall
})
}
}
})
}

function defineLength<F extends Function>(length: number, fn: F) {
return Object.defineProperty(fn, "length", {
value: length,
configurable: true
})
}

function fnApply(options: {
readonly self: any
readonly body: Function
Expand Down Expand Up @@ -13847,14 +13853,17 @@ function fnApply(options: {
* @category Tracing
*/
export const fnUntraced: fn.Gen = (body: Function, ...pipeables: Array<any>) =>
pipeables.length === 0
? function(this: any, ...args: Array<any>) {
return core.fromIterator(() => body.apply(this, args))
}
: function(this: any, ...args: Array<any>) {
let effect = core.fromIterator(() => body.apply(this, args))
for (const x of pipeables) {
effect = x(effect)
defineLength(
body.length,
pipeables.length === 0
? function(this: any, ...args: Array<any>) {
return core.fromIterator(() => body.apply(this, args))
}
return effect
}
: function(this: any, ...args: Array<any>) {
let effect = core.fromIterator(() => body.apply(this, args))
for (const x of pipeables) {
effect = x(effect)
}
return effect
}
)
26 changes: 26 additions & 0 deletions packages/effect/test/Effect/fn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,30 @@ describe("Effect.fn", () => {
assertInstanceOf(cause.right.defect, Error)
strictEqual(cause.right.defect.message, "test2")
}))

it("should preserve the function length", () => {
const f = function*(n: number) {
return n
}
const fn1 = Effect.fn("fn1")(f)
strictEqual(fn1.length, 1)
strictEqual(Effect.runSync(fn1(2)), 2)
const fn2 = Effect.fn(f)
strictEqual(fn2.length, 1)
strictEqual(Effect.runSync(fn2(2)), 2)
})
})

describe("Effect.fnUntraced", () => {
it("should preserve the function length", () => {
const f = function*(n: number) {
return n
}
const fn1 = Effect.fnUntraced(f)
strictEqual(fn1.length, 1)
strictEqual(Effect.runSync(fn1(2)), 2)
const fn2 = Effect.fnUntraced(f, (x) => x)
strictEqual(fn2.length, 1)
strictEqual(Effect.runSync(fn2(2)), 2)
})
})
2 changes: 1 addition & 1 deletion packages/effect/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const config: ViteUserConfig = {
test: {
coverage: {
reporter: ["html"],
include: ["src/Match.ts", "src/internal/matcher.ts"]
include: ["src/Effect.ts"]
}
}
}
Expand Down

0 comments on commit f70a65a

Please sign in to comment.