Repository: zio/zio
Pull request #9782: Add ZIO.fromFunctionZIO
Author: login: adrian-2414745
Created: 2025-04-13T17:48:54Z | Merged: 2025-04-21T07:18:18Z

Just like ZLayer.fromFunction.

The main pain point which triggered me was that in ZIO test i had to access all services individually

```
test() {
  a <- ZIO.service[A]
  b <- ZIO.service[b]
  // use a and b
}
```

now it is possible to get many services in one go

```
test() { 
  ZIO.fromFunction { (a: A, b: B) =>
     //use a and b
  }
}
```


Diff:
diff --git a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala
index f8cd47cef8e2..e2fbc9fe257d 100644
--- a/core-tests/shared/src/test/scala/zio/ZIOSpec.scala
+++ b/core-tests/shared/src/test/scala/zio/ZIOSpec.scala
@@ -3753,6 +3753,103 @@ object ZIOSpec extends ZIOBaseSpec {
         assertZIO(zio.provide(ZLayer.succeed(0)))(equalTo(3))
       }
     ),
+    suite("fromFunctionZIO")(
+      test("1 arg") {
+        ZIO.fromFunctionZIO { (int: Int) =>
+          ZIO.succeed(assertTrue(int == 1))
+        }
+          .provide(ZLayer.succeed(1))
+      },
+      test("fromFunction works with environment") {
+        trait MyService {
+          def n: Int
+        }
+
+        ZIO.fromFunctionZIO { (int: Int) =>
+          ZIO
+            .serviceWith[MyService](_.n + int)
+            .map(n => assertTrue(n == 3))
+        }
+          .provide(ZLayer.succeed(2) ++ ZLayer.succeed(new MyService { def n = 1 }))
+      },
+      test("2 arg") {
+        ZIO.fromFunctionZIO { (int: Int, string: String) =>
+          ZIO.succeed(assertTrue(int == 1, string == "ab"))
+        }
+          .provide(ZLayer.succeed(1) ++ ZLayer.succeed("ab"))
+      },
+      test("3 arg") {
+        ZIO.fromFunctionZIO { (int: Int, string: String, double: Double) =>
+          ZIO.succeed(assertTrue(int == 1, string == "ab", double == 3))
+        }
+          .provide(
+            ZLayer.succeed(1) ++
+              ZLayer.succeed("ab") ++
+              ZLayer.succeed(3.toDouble)
+          )
+      },
+      test("4 arg") {
+        ZIO.fromFunctionZIO { (int: Int, string: String, double: Double, byte: Byte) =>
+          ZIO.succeed(assertTrue(int == 1, string == "ab", double == 3, byte == 4))
+        }
+          .provide(
+            ZLayer.succeed(1) ++
+              ZLayer.succeed("ab") ++
+              ZLayer.succeed(3.toDouble) ++
+              ZLayer.succeed(4.toByte)
+          )
+      },
+      test("5 arg") {
+        ZIO.fromFunctionZIO { (int: Int, string: String, double: Double, byte: Byte, c: Char) =>
+          ZIO.succeed(assertTrue(int == 1, string == "ab", double == 3, byte == 4, c == 'c'))
+        }
+          .provide(
+            ZLayer.succeed(1) ++
+              ZLayer.succeed("ab") ++
+              ZLayer.succeed(3.toDouble) ++
+              ZLayer.succeed(4.toByte) ++
+              ZLayer.succeed('c')
+          )
+      },
+      test("6 arg") {
+        ZIO.fromFunctionZIO { (int: Int, string: String, double: Double, byte: Byte, c: Char, li: List[Int]) =>
+          ZIO.succeed(assertTrue(int == 1, string == "ab", double == 3, byte == 4, c == 'c', li == List(1, 2)))
+        }
+          .provide(
+            ZLayer.succeed(1) ++
+              ZLayer.succeed("ab") ++
+              ZLayer.succeed(3.toDouble) ++
+              ZLayer.succeed(4.toByte) ++
+              ZLayer.succeed('c') ++
+              ZLayer.succeed(List(1, 2))
+          )
+      },
+      test("7 arg") {
+        ZIO.fromFunctionZIO {
+          (int: Int, string: String, double: Double, byte: Byte, c: Char, li: List[Int], ls: List[String]) =>
+            ZIO.succeed(
+              assertTrue(
+                int == 1,
+                string == "ab",
+                double == 3,
+                byte == 4,
+                c == 'c',
+                li == List(1, 2),
+                ls == List("a", "b")
+              )
+            )
+        }
+          .provide(
+            ZLayer.succeed(1) ++
+              ZLayer.succeed("ab") ++
+              ZLayer.succeed(3.toDouble) ++
+              ZLayer.succeed(4.toByte) ++
+              ZLayer.succeed('c') ++
+              ZLayer.succeed(List(1, 2)) ++
+              ZLayer.succeed(List("a", "b"))
+          )
+      }
+    ),
     suite("schedule")(
       test("runs effect for each recurrence of the schedule") {
         for {
diff --git a/core/shared/src/main/scala/zio/ZIO.scala b/core/shared/src/main/scala/zio/ZIO.scala
index a0d882f68b0f..d495e5cdb6bf 100644
--- a/core/shared/src/main/scala/zio/ZIO.scala
+++ b/core/shared/src/main/scala/zio/ZIO.scala
@@ -4764,6 +4764,12 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * Builds a ZIO from the specified function.
+   */
+  def fromFunctionZIO[In](in: In)(implicit constructor: ZIOFunctionConstructor[In], trace: Trace): constructor.Out =
+    constructor(in)
+
   /**
    * Returns an effect that shifts execution to the specified executor. This is
    * useful to specify a default executor that effects sequenced after this one
diff --git a/core/shared/src/main/scala/zio/ZIOFunctionConstructor.scala b/core/shared/src/main/scala/zio/ZIOFunctionConstructor.scala
new file mode 100644
index 000000000000..127dc3055035
--- /dev/null
+++ b/core/shared/src/main/scala/zio/ZIOFunctionConstructor.scala
@@ -0,0 +1,104 @@
+package zio
+
+/**
+ * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from a
+ * function of type `In`. This allows the type of the `ZIO` value constructed to
+ * depend on `In`.
+ */
+sealed abstract class ZIOFunctionConstructor[In] {
+
+  /**
+   * The type of the `ZIO` value.
+   */
+  type Out
+
+  /**
+   * Constructs a `ZIO` value from the specified input.
+   */
+  def apply(in: In)(implicit trace: Trace): Out
+}
+
+object ZIOFunctionConstructor {
+
+  type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+  implicit def function1Constructor[A: Tag, R, E, Z]
+    : ZIOFunctionConstructor.WithOut[A => ZIO[R, E, Z], ZIO[R with A, E, Z]] =
+    new ZIOFunctionConstructor[A => ZIO[R, E, Z]] {
+      type Out = ZIO[R with A, E, Z]
+      def apply(f: A => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A, E, Z] =
+        ZIO.serviceWithZIO[A](f)
+    }
+
+  implicit def function2Constructor[A: Tag, B: Tag, R, E, Z]
+    : ZIOFunctionConstructor.WithOut[(A, B) => ZIO[R, E, Z], ZIO[R with A with B, E, Z]] =
+    new ZIOFunctionConstructor[(A, B) => ZIO[R, E, Z]] {
+      type Out = ZIO[R with A with B, E, Z]
+      def apply(f: (A, B) => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A with B, E, Z] =
+        ZIO.environmentWithZIO[A with B](env => f(env.get[A], env.get[B]))
+    }
+
+  implicit def function3Constructor[A: Tag, B: Tag, C: Tag, R, E, Z]
+    : ZIOFunctionConstructor.WithOut[(A, B, C) => ZIO[R, E, Z], ZIO[R with A with B with C, E, Z]] =
+    new ZIOFunctionConstructor[(A, B, C) => ZIO[R, E, Z]] {
+      type Out = ZIO[R with A with B with C, E, Z]
+      def apply(f: (A, B, C) => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A with B with C, E, Z] =
+        ZIO.environmentWithZIO[A with B with C](env => f(env.get[A], env.get[B], env.get[C]))
+    }
+
+  implicit def function4Constructor[A: Tag, B: Tag, C: Tag, D: Tag, R, E, Z]
+    : ZIOFunctionConstructor.WithOut[(A, B, C, D) => ZIO[R, E, Z], ZIO[R with A with B with C with D, E, Z]] =
+    new ZIOFunctionConstructor[(A, B, C, D) => ZIO[R, E, Z]] {
+      type Out = ZIO[R with A with B with C with D, E, Z]
+      def apply(f: (A, B, C, D) => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A with B with C with D, E, Z] =
+        ZIO.environmentWithZIO[A with B with C with D](env => f(env.get[A], env.get[B], env.get[C], env.get[D]))
+    }
+
+  implicit def function5Constructor[A: Tag, B: Tag, C: Tag, D: Tag, F: Tag, R, E, Z]
+    : ZIOFunctionConstructor.WithOut[(A, B, C, D, F) => ZIO[R, E, Z], ZIO[
+      R with A with B with C with D with F,
+      E,
+      Z
+    ]] =
+    new ZIOFunctionConstructor[(A, B, C, D, F) => ZIO[R, E, Z]] {
+      type Out = ZIO[R with A with B with C with D with F, E, Z]
+      def apply(
+        f: (A, B, C, D, F) => ZIO[R, E, Z]
+      )(implicit trace: Trace): ZIO[R with A with B with C with D with F, E, Z] =
+        ZIO.environmentWithZIO[A with B with C with D with F](env =>
+          f(env.get[A], env.get[B], env.get[C], env.get[D], env.get[F])
+        )
+    }
+
+  implicit def function6Constructor[A: Tag, B: Tag, C: Tag, D: Tag, F: Tag, G: Tag, R, E, Z]
+    : ZIOFunctionConstructor.WithOut[(A, B, C, D, F, G) => ZIO[R, E, Z], ZIO[
+      R with A with B with C with D with F with G,
+      E,
+      Z
+    ]] =
+    new ZIOFunctionConstructor[(A, B, C, D, F, G) => ZIO[R, E, Z]] {
+      type Out = ZIO[R with A with B with C with D with F with G, E, Z]
+      def apply(
+        f: (A, B, C, D, F, G) => ZIO[R, E, Z]
+      )(implicit trace: Trace): ZIO[R with A with B with C with D with F with G, E, Z] =
+        ZIO.environmentWithZIO[A with B with C with D with F with G](env =>
+          f(env.get[A], env.get[B], env.get[C], env.get[D], env.get[F], env.get[G])
+        )
+    }
+
+  implicit def function7Constructor[A: Tag, B: Tag, C: Tag, D: Tag, F: Tag, G: Tag, H: Tag, R, E, Z]
+    : ZIOFunctionConstructor.WithOut[(A, B, C, D, F, G, H) => ZIO[R, E, Z], ZIO[
+      R with A with B with C with D with F with G with H,
+      E,
+      Z
+    ]] =
+    new ZIOFunctionConstructor[(A, B, C, D, F, G, H) => ZIO[R, E, Z]] {
+      type Out = ZIO[R with A with B with C with D with F with G with H, E, Z]
+      def apply(
+        f: (A, B, C, D, F, G, H) => ZIO[R, E, Z]
+      )(implicit trace: Trace): ZIO[R with A with B with C with D with F with G with H, E, Z] =
+        ZIO.environmentWithZIO[A with B with C with D with F with G with H](env =>
+          f(env.get[A], env.get[B], env.get[C], env.get[D], env.get[F], env.get[G], env.get[H])
+        )
+    }
+}


Reviews:
user: login: hearnadam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T23:31:40Z
user: login: kyri-petrou
body: Love it! Too bad we'll have overlap between `ZIO.serviceWith` and `ZIO.fromFunction` for the Function1 case but I think that should be OK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T08:24:46Z
user: login: hearnadam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T08:48:17Z
user: login: adrian-2414745
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:15:08Z
user: login: adrian-2414745
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:15:25Z
user: login: adrian-2414745
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:15:37Z
user: login: adrian-2414745
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:15:53Z
user: login: hearnadam
body: Change overall LGTM - though it appears you had a faulty merge, including some other commits in your history.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T08:27:05Z
user: login: adrian-2414745
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T08:48:06Z
user: login: hearnadam
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-16T00:01:17Z
user: login: adrian-2414745
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T05:50:43Z
user: login: kyri-petrou
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T14:29:58Z
user: login: adrian-2414745
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T17:57:46Z
user: login: kyri-petrou
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T07:44:18Z
user: login: kyri-petrou
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T07:45:08Z
user: login: adrian-2414745
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T09:15:47Z
user: login: kyri-petrou
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T18:53:17Z
user: login: adrian-2414745
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T16:21:28Z
user: login: hearnadam
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-21T00:31:35Z
user: login: kyri-petrou
body: Thank you! Looks like a great addition!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T07:18:03Z

Review comments:
diff_hunk: @@ -4764,6 +4764,123 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  trait ZIOFunctionConstructor[In] {
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: hearnadam
body: ```suggestion
  sealed abstract class ZIOFunctionConstructor[In] {
```
created_at: 2025-04-13T23:28:14Z
author_association: COLLABORATOR
diff_hunk: @@ -4764,6 +4764,123 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  trait ZIOFunctionConstructor[In] {
+
+    /**
+     * The type of the `ZIO` value.
+     */
+    type Out
+
+    /**
+     * Constructs a `ZIO` value from the specified input.
+     */
+    def apply(in: In)(implicit trace: Trace): Out
+  }
+
+  object ZIOFunctionConstructor {
+
+    type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+    implicit def function1Constructor[A: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[A => ZIO[R, E, Z], ZIO[R with A, E, Z]] =
+      new ZIOFunctionConstructor[A => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A, E, Z]
+        def apply(f: A => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A, E, Z] =
+          ZIO.serviceWith[A](a => f(a)).flatten
+
+      }
+
+    implicit def function2Constructor[A: Tag, B: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[(A, B) => ZIO[R, E, Z], ZIO[R with A with B, E, Z]] =
+      new ZIOFunctionConstructor[(A, B) => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A with B, E, Z]
+        def apply(f: (A, B) => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A with B, E, Z] =
+          for {
+            a <- ZIO.service[A]
+            b <- ZIO.service[B]
+            z <- f(a, b)
+          } yield z
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: hearnadam
body: This can be done in a single suspension:

```suggestion
            ZIO.environmentWith[A with B] { env =>
            	f(a, b)
            }
```

Goes for all the other variants 🙂 
created_at: 2025-04-13T23:30:30Z
author_association: COLLABORATOR
diff_hunk: @@ -4764,6 +4764,123 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  trait ZIOFunctionConstructor[In] {
+
+    /**
+     * The type of the `ZIO` value.
+     */
+    type Out
+
+    /**
+     * Constructs a `ZIO` value from the specified input.
+     */
+    def apply(in: In)(implicit trace: Trace): Out
+  }
+
+  object ZIOFunctionConstructor {
+
+    type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+    implicit def function1Constructor[A: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[A => ZIO[R, E, Z], ZIO[R with A, E, Z]] =
+      new ZIOFunctionConstructor[A => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A, E, Z]
+        def apply(f: A => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A, E, Z] =
+          ZIO.serviceWith[A](a => f(a)).flatten
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: kyri-petrou
body: Doesn't `ZIO.serviceWithZIO[A](f)` work as well?
created_at: 2025-04-14T08:18:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4764,6 +4764,123 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  trait ZIOFunctionConstructor[In] {
+
+    /**
+     * The type of the `ZIO` value.
+     */
+    type Out
+
+    /**
+     * Constructs a `ZIO` value from the specified input.
+     */
+    def apply(in: In)(implicit trace: Trace): Out
+  }
+
+  object ZIOFunctionConstructor {
+
+    type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+    implicit def function1Constructor[A: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[A => ZIO[R, E, Z], ZIO[R with A, E, Z]] =
+      new ZIOFunctionConstructor[A => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A, E, Z]
+        def apply(f: A => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A, E, Z] =
+          ZIO.serviceWith[A](a => f(a)).flatten
+
+      }
+
+    implicit def function2Constructor[A: Tag, B: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[(A, B) => ZIO[R, E, Z], ZIO[R with A with B, E, Z]] =
+      new ZIOFunctionConstructor[(A, B) => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A with B, E, Z]
+        def apply(f: (A, B) => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A with B, E, Z] =
+          for {
+            a <- ZIO.service[A]
+            b <- ZIO.service[B]
+            z <- f(a, b)
+          } yield z
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: kyri-petrou
body: Yes please, this way we avoid all the unnecessary flatmaps

Just a minor correction, I think the code would be:
```scala
f(env.get[A], env.get[B])
```
created_at: 2025-04-14T08:21:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3753,6 +3753,32 @@ object ZIOSpec extends ZIOBaseSpec {
         assertZIO(zio.provide(ZLayer.succeed(0)))(equalTo(3))
       }
     ),
+    suite("fromFunction")(
+      test("1 arg") {
+        ZIO.fromFunction { (int: Int) =>
+          ZIO.succeed(assertTrue(int == 1))
+        }
+          .provide(ZLayer.succeed(1))
+      },
+      test("fromFunction works with environment") {
+        trait MyService {
+          def n: Int
+        }
+
+        ZIO.fromFunction { (int: Int) =>
+          ZIO
+            .serviceWith[MyService](_.n + int)
+            .map(n => assertTrue(n == 3))
+        }
+          .provide(ZLayer.succeed(2) ++ ZLayer.succeed(new MyService { def n = 1 }))
+      },
+      test("2 arg") {
path: core-tests/shared/src/test/scala/zio/ZIOSpec.scala
user: login: kyri-petrou
body: Can we add tests for all arities? 🙏 
created_at: 2025-04-14T08:23:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4764,6 +4764,123 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  trait ZIOFunctionConstructor[In] {
+
+    /**
+     * The type of the `ZIO` value.
+     */
+    type Out
+
+    /**
+     * Constructs a `ZIO` value from the specified input.
+     */
+    def apply(in: In)(implicit trace: Trace): Out
+  }
+
+  object ZIOFunctionConstructor {
+
+    type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+    implicit def function1Constructor[A: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[A => ZIO[R, E, Z], ZIO[R with A, E, Z]] =
+      new ZIOFunctionConstructor[A => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A, E, Z]
+        def apply(f: A => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A, E, Z] =
+          ZIO.serviceWith[A](a => f(a)).flatten
+
+      }
+
+    implicit def function2Constructor[A: Tag, B: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[(A, B) => ZIO[R, E, Z], ZIO[R with A with B, E, Z]] =
+      new ZIOFunctionConstructor[(A, B) => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A with B, E, Z]
+        def apply(f: (A, B) => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A with B, E, Z] =
+          for {
+            a <- ZIO.service[A]
+            b <- ZIO.service[B]
+            z <- f(a, b)
+          } yield z
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: hearnadam
body: Yep, thanks
created_at: 2025-04-14T08:48:17Z
author_association: COLLABORATOR
diff_hunk: @@ -4764,6 +4764,123 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  trait ZIOFunctionConstructor[In] {
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: adrian-2414745
body: Done
created_at: 2025-04-15T08:15:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4764,6 +4764,123 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  trait ZIOFunctionConstructor[In] {
+
+    /**
+     * The type of the `ZIO` value.
+     */
+    type Out
+
+    /**
+     * Constructs a `ZIO` value from the specified input.
+     */
+    def apply(in: In)(implicit trace: Trace): Out
+  }
+
+  object ZIOFunctionConstructor {
+
+    type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+    implicit def function1Constructor[A: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[A => ZIO[R, E, Z], ZIO[R with A, E, Z]] =
+      new ZIOFunctionConstructor[A => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A, E, Z]
+        def apply(f: A => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A, E, Z] =
+          ZIO.serviceWith[A](a => f(a)).flatten
+
+      }
+
+    implicit def function2Constructor[A: Tag, B: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[(A, B) => ZIO[R, E, Z], ZIO[R with A with B, E, Z]] =
+      new ZIOFunctionConstructor[(A, B) => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A with B, E, Z]
+        def apply(f: (A, B) => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A with B, E, Z] =
+          for {
+            a <- ZIO.service[A]
+            b <- ZIO.service[B]
+            z <- f(a, b)
+          } yield z
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: adrian-2414745
body: Done for all
created_at: 2025-04-15T08:15:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4764,6 +4764,123 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  trait ZIOFunctionConstructor[In] {
+
+    /**
+     * The type of the `ZIO` value.
+     */
+    type Out
+
+    /**
+     * Constructs a `ZIO` value from the specified input.
+     */
+    def apply(in: In)(implicit trace: Trace): Out
+  }
+
+  object ZIOFunctionConstructor {
+
+    type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+    implicit def function1Constructor[A: Tag, Z: Tag, R, E]
+      : ZIOFunctionConstructor.WithOut[A => ZIO[R, E, Z], ZIO[R with A, E, Z]] =
+      new ZIOFunctionConstructor[A => ZIO[R, E, Z]] {
+        type Out = ZIO[R with A, E, Z]
+        def apply(f: A => ZIO[R, E, Z])(implicit trace: Trace): ZIO[R with A, E, Z] =
+          ZIO.serviceWith[A](a => f(a)).flatten
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: adrian-2414745
body: done
created_at: 2025-04-15T08:15:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3753,6 +3753,32 @@ object ZIOSpec extends ZIOBaseSpec {
         assertZIO(zio.provide(ZLayer.succeed(0)))(equalTo(3))
       }
     ),
+    suite("fromFunction")(
+      test("1 arg") {
+        ZIO.fromFunction { (int: Int) =>
+          ZIO.succeed(assertTrue(int == 1))
+        }
+          .provide(ZLayer.succeed(1))
+      },
+      test("fromFunction works with environment") {
+        trait MyService {
+          def n: Int
+        }
+
+        ZIO.fromFunction { (int: Int) =>
+          ZIO
+            .serviceWith[MyService](_.n + int)
+            .map(n => assertTrue(n == 3))
+        }
+          .provide(ZLayer.succeed(2) ++ ZLayer.succeed(new MyService { def n = 1 }))
+      },
+      test("2 arg") {
path: core-tests/shared/src/test/scala/zio/ZIOSpec.scala
user: login: adrian-2414745
body: done
created_at: 2025-04-15T08:15:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4764,6 +4764,112 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  sealed abstract class ZIOFunctionConstructor[In] {
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: hearnadam
body: Should this be it's own file?
created_at: 2025-04-15T08:26:35Z
author_association: COLLABORATOR
diff_hunk: @@ -4764,6 +4764,112 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  sealed abstract class ZIOFunctionConstructor[In] {
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: adrian-2414745
body: it can. should it be in zio or zio.internal package ?
created_at: 2025-04-15T08:48:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4764,6 +4764,112 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  sealed abstract class ZIOFunctionConstructor[In] {
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: hearnadam
body: `zio` should be fine
created_at: 2025-04-16T00:01:17Z
author_association: COLLABORATOR
diff_hunk: @@ -4764,6 +4764,112 @@ object ZIO extends ZIOCompanionPlatformSpecific with ZIOCompanionVersionSpecific
   def serviceWithZIO[Service]: ServiceWithZIOPartiallyApplied[Service] =
     new ServiceWithZIOPartiallyApplied[Service]
 
+  /**
+   * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from
+   * a function of type `In`. This allows the type of the `ZIO` value
+   * constructed to depend on `In`.
+   */
+  sealed abstract class ZIOFunctionConstructor[In] {
path: core/shared/src/main/scala/zio/ZIO.scala
user: login: adrian-2414745
body: done
created_at: 2025-04-16T05:50:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3753,6 +3753,93 @@ object ZIOSpec extends ZIOBaseSpec {
         assertZIO(zio.provide(ZLayer.succeed(0)))(equalTo(3))
       }
     ),
+    suite("fromFunctionZIO")(
+      test("1 arg") {
+        ZIO.fromFunctionZIO { (int: Int) =>
+          ZIO.succeed(assertTrue(int == 1))
+        }
+          .provide(ZLayer.succeed(1))
+      },
+      test("fromFunction works with environment") {
+        trait MyService {
+          def n: Int
+        }
+
+        ZIO.fromFunctionZIO { (int: Int) =>
+          ZIO
+            .serviceWith[MyService](_.n + int)
+            .map(n => assertTrue(n == 3))
+        }
+          .provide(ZLayer.succeed(2) ++ ZLayer.succeed(new MyService { def n = 1 }))
+      },
+      test("2 arg") {
+        ZIO.fromFunctionZIO { (int: Int, string: String) =>
+          ZIO.succeed(assertTrue((int + string.length) == 3))
path: core-tests/shared/src/test/scala/zio/ZIOSpec.scala
user: login: kyri-petrou
body: Why not test each argument explicitly? (In other tests as well)

```suggestion
          ZIO.succeed(assertTrue(int == 1, string == "ab"))
```
created_at: 2025-04-16T14:29:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3753,6 +3753,93 @@ object ZIOSpec extends ZIOBaseSpec {
         assertZIO(zio.provide(ZLayer.succeed(0)))(equalTo(3))
       }
     ),
+    suite("fromFunctionZIO")(
+      test("1 arg") {
+        ZIO.fromFunctionZIO { (int: Int) =>
+          ZIO.succeed(assertTrue(int == 1))
+        }
+          .provide(ZLayer.succeed(1))
+      },
+      test("fromFunction works with environment") {
+        trait MyService {
+          def n: Int
+        }
+
+        ZIO.fromFunctionZIO { (int: Int) =>
+          ZIO
+            .serviceWith[MyService](_.n + int)
+            .map(n => assertTrue(n == 3))
+        }
+          .provide(ZLayer.succeed(2) ++ ZLayer.succeed(new MyService { def n = 1 }))
+      },
+      test("2 arg") {
+        ZIO.fromFunctionZIO { (int: Int, string: String) =>
+          ZIO.succeed(assertTrue((int + string.length) == 3))
path: core-tests/shared/src/test/scala/zio/ZIOSpec.scala
user: login: adrian-2414745
body: done
created_at: 2025-04-16T17:57:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+package zio
+
+/**
+ * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from a
+ * function of type `In`. This allows the type of the `ZIO` value constructed to
+ * depend on `In`.
+ */
+sealed abstract class ZIOFunctionConstructor[In] {
+
+  /**
+   * The type of the `ZIO` value.
+   */
+  type Out
+
+  /**
+   * Constructs a `ZIO` value from the specified input.
+   */
+  def apply(in: In)(implicit trace: Trace): Out
+}
+
+object ZIOFunctionConstructor {
+
+  type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+  implicit def function1Constructor[A: Tag, Z: Tag, R, E]
path: core/shared/src/main/scala/zio/ZIOFunctionConstructor.scala
user: login: kyri-petrou
body: Really sorry for not picking this earlier and causing extra rounds of reviews/comments.

I think the `Tag` implicit on Z is unnecessary (same for all other methods)

```suggestion
  implicit def function1Constructor[A: Tag, Z, R, E]
```
created_at: 2025-04-17T07:44:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+package zio
+
+/**
+ * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from a
+ * function of type `In`. This allows the type of the `ZIO` value constructed to
+ * depend on `In`.
+ */
+sealed abstract class ZIOFunctionConstructor[In] {
+
+  /**
+   * The type of the `ZIO` value.
+   */
+  type Out
+
+  /**
+   * Constructs a `ZIO` value from the specified input.
+   */
+  def apply(in: In)(implicit trace: Trace): Out
+}
+
+object ZIOFunctionConstructor {
+
+  type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+  implicit def function1Constructor[A: Tag, Z: Tag, R, E]
path: core/shared/src/main/scala/zio/ZIOFunctionConstructor.scala
user: login: kyri-petrou
body: Also by convention we generally place the output type param at the rightmost side, i.e., `[A: Tag, R, E, Z]`
created_at: 2025-04-17T07:45:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+package zio
+
+/**
+ * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from a
+ * function of type `In`. This allows the type of the `ZIO` value constructed to
+ * depend on `In`.
+ */
+sealed abstract class ZIOFunctionConstructor[In] {
+
+  /**
+   * The type of the `ZIO` value.
+   */
+  type Out
+
+  /**
+   * Constructs a `ZIO` value from the specified input.
+   */
+  def apply(in: In)(implicit trace: Trace): Out
+}
+
+object ZIOFunctionConstructor {
+
+  type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+  implicit def function1Constructor[A: Tag, Z: Tag, R, E]
path: core/shared/src/main/scala/zio/ZIOFunctionConstructor.scala
user: login: adrian-2414745
body: done
created_at: 2025-04-17T09:15:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+package zio
+
+/**
+ * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from a
+ * function of type `In`. This allows the type of the `ZIO` value constructed to
+ * depend on `In`.
+ */
+sealed abstract class ZIOFunctionConstructor[In] {
+
+  /**
+   * The type of the `ZIO` value.
+   */
+  type Out
+
+  /**
+   * Constructs a `ZIO` value from the specified input.
+   */
+  def apply(in: In)(implicit trace: Trace): Out
+}
+
+object ZIOFunctionConstructor {
+
+  type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+  implicit def function1Constructor[A: Tag, R, E, Z: Tag]
path: core/shared/src/main/scala/zio/ZIOFunctionConstructor.scala
user: login: kyri-petrou
body: I think you missed my previous comment. The `: Tag` on `Z` is unnecessary as far as I can tell (in this and other methods)
created_at: 2025-04-17T18:52:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+package zio
+
+/**
+ * A `ZIOFunctionConstructor[Input]` knows how to construct a `ZIO` value from a
+ * function of type `In`. This allows the type of the `ZIO` value constructed to
+ * depend on `In`.
+ */
+sealed abstract class ZIOFunctionConstructor[In] {
+
+  /**
+   * The type of the `ZIO` value.
+   */
+  type Out
+
+  /**
+   * Constructs a `ZIO` value from the specified input.
+   */
+  def apply(in: In)(implicit trace: Trace): Out
+}
+
+object ZIOFunctionConstructor {
+
+  type WithOut[In, Out0] = ZIOFunctionConstructor[In] { type Out = Out0 }
+
+  implicit def function1Constructor[A: Tag, R, E, Z: Tag]
path: core/shared/src/main/scala/zio/ZIOFunctionConstructor.scala
user: login: adrian-2414745
body: missed it. done
created_at: 2025-04-19T16:21:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: CLAassistant
created_at: 2025-04-13T17:49:00Z
body: [![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/zio/zio?pullRequest=9782) <br/>All committers have signed the CLA.
author_association: NONE
user: login: kyri-petrou
created_at: 2025-04-14T08:26:30Z
body: One more comment: Can we rename this method to `fromFunctionZIO` to keep naming aligned with `ZIO.fromServiceZIO`? We potentially want to also add `fromFunction` where the method something that is not effectual.
author_association: CONTRIBUTOR
user: login: adrian-2414745
created_at: 2025-04-15T08:46:17Z
body: > Change overall LGTM - though it appears you had a faulty merge, including some other commits in your history.

Yeah, i'm confused why the new files appear in this PR.
I think I did a git pull with merge, then git reset --soft to the commit before my original commit.
After the git reset, I had these extra files to commit again
author_association: CONTRIBUTOR
user: login: adrian-2414745
created_at: 2025-04-15T14:08:57Z
body: > Change overall LGTM - though it appears you had a faulty merge, including some other commits in your history.

fixed merge
author_association: CONTRIBUTOR
user: login: hearnadam
created_at: 2025-04-16T06:58:21Z
body: @adrian-2414745 can you run fmt?
author_association: COLLABORATOR
user: login: adrian-2414745
created_at: 2025-04-16T07:06:31Z
body: > @adrian-2414745 can you run fmt?

done.
author_association: CONTRIBUTOR