Providing environments while using Environment Functions

Within kyo, there are a few different ways you’d commonly see environments being provided. Here are a couple:

Env.run(a)(Env.run(b)(Env.run(c)(fn)))

fn.handle(
  Env.run(a),
  Env.run(b),
  Env.run(c)
)

Env.runAll(TypeMap(a,b,c))(fn)

At the time of writing, none of these should be used in conjunction with Environment Functions without type annotations. You’ll usually get compile time errors, but in rare circumstances you’ll get a runtime exception that’s explicitly marked as a bug. I’ve opened a bug report (as requested by the compiler).

So long as you always explicitly provide the type arguments to a function before you provide an environment it works in most cases, and importantly when it doesn’t you should get a compiler error (not a runtime error).

So let’s take the original article’s fetchAndSaveGame function, it would be nice to be able to write:

fetchAndSaveGame(1).handle(
  Env.run(httpFetchGame),
  Env.run(postgresSaveGame),
  // handle other effects here, like other Env.run or Abort.run
)

and hopefully in the future this version does actually compile/run successfully. The following sections will outline a few viable options in the meantime.

Fully specify the type by hand at the callsite Link to heading

type FetchGame[S] = Long => Game < S
type SaveGame[S] = Game => Long < S

def httpFetchGame(gameId: Long) =
  Requests(_.response(asJson[Game]).get(uri"https://somegame.com/games/$gameId"))

def postgresSaveGame(game: Game) =
  Env.use[Postgres[SnakeCase]]: ctx =>
    import ctx.*
    ZIOs.get(run(query.insertValue(lift(game))))

def fetchAndSaveGame[S](gameId: Long) =
  for
    game <- Env.use[FetchGame[S]](_(gameId))
    _ <- Env.use[SaveGame[S]](_(game))
  yield game

object Main extends KyoApp:
  run:
    fetchAndSaveGame[Async & Env[Postgres[SnakeCase]]
      Abort[FailedRequest | ResponseException[String, Error] | SQLException]
    ](1).handle(
      Env.run(httpFetchGame),
      Env.run(postgresSaveGame),
      Env.run(??? : Postgres[SnakeCase]),
      Abort.run
    )

In this solution you don’t have to change any of the core/implementations. You just check the types of httpFetchGame and postgresSaveGame in your editor, and then manually combine the effects by hand when you call fetchAndSaveGame.

Use type aliases to manually specify the effect types. Link to heading


type FetchGame[S] = Long => Game < S
type SaveGame[S] = Game => Long < S

type HttpFetchGameEff = Async & 
  Abort[FailedRequest | ResponseException[String, Error]]
val httpFetchGame: FetchGame[HttpFetchGamEff] = gameId =>
  Requests(_.response(asJson[Game]).get(uri"https://somegame.com/games/$gameId"))

type PostgresSaveGameEff = Async & Abort[SQLException] & Env[Postgres[SnakeCase]]
val postgresSaveGame: SaveGame[PostgresSaveGameEff] = game =>
  Env.get[Postgres[SnakeCase]].map: ctx =>
    import ctx.*
    ZIOs.get(run(query.insertValue(lift(game))))

def fetchAndSaveGame[S](gameId: Long) =
  for
    game <- Env.use[FetchGame[S]](_(gameId))
    _ <- Env.use[SaveGame[S]](_(game))
  yield game

object Main extends KyoApp:
  run:
    fetchAndSaveGame[HttpFetchGameEff & PostgresSaveGameEff](1).handle(
      Env.run(httpFetchGame),
      Env.run(postgresSaveGame),
      Env.run(??? : Postgres[SnakeCase]),
      Abort.run
    )

We change our defs to vals because we don’t want to define functions that return functions unnecessarily.

This is a reasonable solution if you like seeing the types in the code.

Define an effect type extractor operator Link to heading


type <#[X] = X match
  case (_ => _ < s) => s
  case _ < s => s

type FetchGame[S] = Long => Game < S
type SaveGame[S] = Game => Long < S

val httpFetchGame = (gameId: Long) =>
  Requests(_.response(asJson[Game]).get(uri"https://somegame.com/games/$gameId"))

val postgresSaveGame = (game: Game) =>
  Env.use[Postgres[SnakeCase]]: ctx =>
    import ctx.*
    ZIOs.get(run(query.insertValue(lift(game))))

def fetchAndSaveGame[S](gameId: Long) =
  for
    game <- Env.use[FetchGame[S]](_(gameId))
    _ <- Env.use[SaveGame[S]](_(game))
  yield game

object Main extends KyoApp:
  run:
    fetchAndSaveGame[<#[httpFetchGame.type] & <#[postgresSaveGame.type]](1).handle(
      Env.run(httpFetchGame),
      Env.run(postgresSaveGame),
      Env.run(??? : Postgres[SnakeCase]),
      Abort.run
    )

We have to still change our defs to vals because we can’t use type operators on defs. You could also name the operator something like EffectsOf instead of <#, and you could also define similar operators like #< that retrieve the left hand side (result type) instead of the right hand side (effect type), if you ever found it necessary.

This is the only version you don’t need to manually specify the effect types of your implementations anywhere in the code.

You can even get fancier with it and define type operators like this to save you a few characters at each call site:

type <##[X] = X match
  case (_ => _ < s) *: EmptyTuple => s
  case (_ < s) *: EmptyTuple => s
  case (_ => _ < s) *: r => s & <##[r]
  case (_ < s) *: r => s & <##[r]

fn[<#[a.type] & <#[b.type] & <#[c.type] & <#[d.type]]
// vs.
fn[<##[(a.type, b.type, c.type, d.type)]