{"id":21399429,"url":"https://github.com/jcouyang/luci","last_synced_at":"2025-07-13T20:33:38.766Z","repository":{"id":57743796,"uuid":"173034383","full_name":"jcouyang/luci","owner":"jcouyang","description":"Extensible Free Monad Effects","archived":false,"fork":false,"pushed_at":"2021-03-16T23:11:22.000Z","size":166,"stargazers_count":15,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-05-02T00:38:51.297Z","etag":null,"topics":["birds","effects","free-monad","functional-programming","monad","monad-transformers","mtl","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jcouyang.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-02-28T03:29:12.000Z","updated_at":"2023-01-12T01:11:01.000Z","dependencies_parsed_at":"2022-09-11T09:40:31.162Z","dependency_job_id":null,"html_url":"https://github.com/jcouyang/luci","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcouyang%2Fluci","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcouyang%2Fluci/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcouyang%2Fluci/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcouyang%2Fluci/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jcouyang","download_url":"https://codeload.github.com/jcouyang/luci/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225917080,"owners_count":17544831,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["birds","effects","free-monad","functional-programming","monad","monad-transformers","mtl","scala"],"created_at":"2024-11-22T15:14:31.076Z","updated_at":"2024-11-22T15:14:31.715Z","avatar_url":"https://github.com/jcouyang.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# \u003cruby\u003e\u003crb\u003e鸕鶿\u003c/rb\u003e\u003crt\u003elu ci\u003c/rt\u003e\u003c/ruby\u003e\n\nExtensible Free Monad Effects\n\nhttps://blog.oyanglul.us/scala/3-layer-cake\n\n[\u003cimg src=https://upload.wikimedia.org/wikipedia/commons/0/0a/Imperial_Encyclopaedia_-_Animal_Kingdom_-_pic057_-_%E9%B8%95%E9%B6%BF%E5%9C%96.svg width=50%/\u003e](https://en.wikisource.org/wiki/zh:%E5%8F%A4%E4%BB%8A%E5%9C%96%E6%9B%B8%E9%9B%86%E6%88%90/%E5%8D%9A%E7%89%A9%E5%BD%99%E7%B7%A8/%E7%A6%BD%E8%9F%B2%E5%85%B8/%E7%AC%AC045%E5%8D%B7)\n\n**Do one thing and do it well** micro [birds](https://github.com/search?q=org%3Ajcouyang+topic%3Abirds\u0026type=Repositories) library series\n\n![](https://index.scala-lang.org/jcouyang/luci/latest.svg?v=1)\n\n```\nlibraryDependencies += \"us.oyanglul\" %% \"luci\" % \u003cversion\u003e\"\n```\n\n\u003c!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-generate-toc again --\u003e\n**Table of Contents**\n\n- [\u003cruby\u003e\u003crb\u003e鸕鶿\u003c/rb\u003e\u003crt\u003elu ci\u003c/rt\u003e\u003c/ruby\u003e](#rubyrb鸕鶿rbrtlu-cirtruby)\n    - [The Problem](#the-problem)\n    - [The Ultimate Solution](#the-ultimate-solution)\n        - [Some Effects out of the box](#some-effects-out-of-the-box)\n        - [Step 1: Create DSL](#step-1-create-dsl)\n        - [Step 2: Compile the Program](#step-2-compile-the-program)\n        - [Step 3: Run Program](#step-3-run-program)\n    - [Create Your Own Effect](#create-your-own-effect)\n        - [Step 1: Create Data Type](#step-1-create-data-type)\n        - [Step 2: Create Compiler](#step-2-create-compiler)\n        - [Step 3 Use the effect](#step-3-use-the-effect)\n\n\u003c!-- markdown-toc end --\u003e\n\n## The Problem\nWhen you want to mix in some native effects into your Free Monad DSL, some effects won't work\n\nfor instance, we have two effects IO and StateT, and we would like to do some math using StateT\n\nHere is the Program\n```scala\nimport Free.{liftInject =\u003e free}\ntype Program[A] = EitherK[IO, StateT[IO, Int, ?], A]\ntype ProgramF[A] = Free[Program, A]\n\ndef program : Program[Int] = for {\n  initState \u003c- free[Program](StateT.get[IO, Int])\n  _ \u003c- free[Program](IO(println(s\"init state is $initState\")))\n  _ \u003c- free[Program](StateT.modify[IO, Int](_ + 1))\n  res \u003c- free[Program](StateT.modify[IO, Int](_ + 1))\n} yield res\n```\n\nand Interpreters\n```scala\ndef ioInterp = FunctionK.id[IO]\ndef stateTInterp(initState: Int) = Lambda[StateT[IO, Int, ?] ~\u003e IO[(Int, ?)]] { _.run(initState)}\n```\n\nIf we run the program\n```scala\nprogram foldMap (ioInterp or stateTInterp(0))\n```\n\nGuess what, it doesn't work, the result will be 1 not 2\n\nbecause we run the state for each effect separately in the stateTInterp\n\nOne of the option is to use [FreeT](https://typelevel.org/cats/datatypes/freemonad.html#freet)\n\nBut with FreeT:\n\n- you can only mixin 1 effect, what if I have multiple effects that I want them to be stateful across the whole program.\n- all other effects need to be lift to FreeT as well. It might have huge impact to our existing code base that is Free already.\n\n## The Ultimate Solution\n\nis using both [meow-mtl](https://github.com/oleg-py/meow-mtl) and ReaderT/Kleisli, then we can easily integrate mtl into Free Monad Effects\n\n1. instead of using interpreter `Program ~\u003e IO`, we can use `Program ~\u003e Kleisli[IO, ProgramContext, ?]`, and we have a better name for it - *Compiler*\n2. init state of stateful effects can then be injected into program via ProgramContext when actually running `Kleisli[IO, ProgramContext, ?]`\n\n### Some Effects out of the box\n- Id\n- WriterT\n- ReaderT/Kleisli\n- StateT\n- EitherT\n- Http4sClient\n- Doobie ConnectionIO\n- fs2\n\nIt's very similar but just one more step to run the Kleisli\n\n1. create Program dsl\n2. **compile** Program into a Kleisli\n3. **run** Kleisli in a context\n\n\n### Step 1: Create DSL\n\ne.g. our `Program` has lot of effects... WriterT, Http4sClient, ReaderT, IO, StateT and Doobie's ConnectionIO\n\nfew of them need to be stateful across all over the program like WriterT, StateT\n```scala\n type Program[A] = Eff7[\n      Http4sClient[IO, ?],\n      WriterT[IO, Chain[String], ?],\n      ReaderT[IO, Config, ?],\n      IO,\n      ConnectionIO,\n      StateT[IO, Int, ?],\n      Either[Throwable, ?],\n      A\n    ]\ntype ProgramF[A] = Free[Program, A]\n```\n\n`EffX` is predefined alias of type to construct multiple kind in `EitherK`\n\nNow lets start using these effects to do our work\n```scala\nval program = for {\n    config \u003c- free[Program](Kleisli.ask[IO, Config])\n    _ \u003c- free[Program](\n    GetStatus[IO](GET(Uri.uri(\"https://blog.oyanglul.us\"))))\n    _ \u003c- free[Program](StateT.modify[IO, Int](1 + _))\n    _ \u003c- free[Program](StateT.modify[IO, Int](1 + _))\n    state \u003c- free[Program](StateT.get[IO, Int])\n    _ \u003c- free[Program](\n    WriterT.tell[IO, Chain[String]](\n      Chain.one(\"config: \" + config.token)))\n    resOrError \u003c- free[Program](sql\"\"\"select true\"\"\".query[Boolean].unique)\n    _ \u003c- free[Program](\n    resOrError.handleError(e =\u003e println(s\"handle db error $e\")))\n    _ \u003c- free[Program](IO(println(s\"im IO...state: $state\")))\n} yield ()\n```\n\n### Step 2: Compile the Program\nif we compile our program, we should get a binary `ProgramBin`\n```scala\nimport us.oyanglul.luci.compilers.io._\nval binary = compile(program)\n```\nimagine that you have a binary of command line tool, when you run it you would probably need to provide some `--args`\n\nsame here, if you want to run `ProgramBin`, which is basically just a Kleisli, we need to provide args with is `ProgramContext`\n\n### Step 3: Run Program\n\nrun the program with real `--args`\n```scala\nval args = (httpclient ::\n    logRef.tellInstance ::\n    config ::\n    Unit ::\n    transactor ::\n    stateRef.stateInstance ::\n    Unit ::\n    HNil)\n\nbinary.run(args)\n```\n\nfor stateful `WriterT` and `StateT` here, we can get `FunctorTell` and `MonadState` instances from `Ref[IO, ?]`\nand inject them into the program via `ProgramContext`\n\neach one corresponds to program's effect's context\n\n1. binary for `Http4sClient[IO, ?]` needs `Client[IO]` to run\n2. binary for `WriterT[IO, Chain[String], ?]` needs `FuntorTell[IO, Chain[String]]`, presented by meow-mtl `.tellInstance`\n3. binary for `ReaderT[IO, Config, ?]` needs `Config` to run\n4. binary for `IO` needs nothing so `Unit`\n5. binary for `ConnectionIO` needs `Transactor[IO]`\n6. binary for `StateT[IO, Int, ?]` needs `MonadState[IO, Int]` to run, which presented here by meow-mtl from `.stateInstance`\n7. binary for `Either[Throwable, ?]` needs nothing so `Unit`\n\n## Create Your Own Effect\n\ncreating a new compilable effect is pretty simple in 2 steps\n\n### Step 1: Create Data Type\nThis is nothing different from creating an effect data type for Free Monad\n\nFor instance, we need a `s3 putObject` Effect\n\n```scala\n\nimport com.amazonaws.services.s3.model.PutObjectResult\n\nsealed trait S3[A]\n\ncase class PutObject(bucketName: String, fileName: String, content: String)\n    extends S3[PutObjectResult]\n```\n\n### Step 2: Create Compiler\n\nTo create a compiler for new data type s3, we'll need to create an instance for type class Compiler\n```scala\ntrait Compiler[F[_], E[_]] {\n  type Env\n  val compile: F ~\u003e Kleisli[E, Env, ?]\n}\n```\n\nWe need a type of `Env` where the program needs to be compile. e.g. S3 need an AWS S3 Client\n\n```scala\ntrait S3Compiler[E[_]] {\n  implicit def s3Compiler(implicit F: Applicative[E]) = new Compiler[S3, E] {\n    type Env = AmazonS3\n    val compile = Lambda[S3 ~\u003e Kleisli[E, Env, ?]] (_ match {\n      case PutObject(bucketName, fileName, content) =\u003e\n        Kleisli(env =\u003e F.pure(env.putObject(bucketName, fileName, content)))\n    })\n  }\n}\n```\n\n### Step 3 Use the effect\n\nto be honest you don't need to make S3Compiler so generic since you may be the only person who using it. But it's a good practic to make every thing as genric as possible.\n\nany way to use the generic effect, you can create a specific object just for IO(or Task of your choice)\n```scala\nobject s3IoCompiler extends S3Compiler[IO]\n```\n\nand then import it to where you need to compile\n```scala\nimport s3IoCompiler._\n```\n\nOr, simply extends it on the object or class you intent to compile your program\n```scala\nobject Main extends S3Compiler[IO] with All{\n  ...\n  val binary = compile(program)\n  ...\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcouyang%2Fluci","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjcouyang%2Fluci","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcouyang%2Fluci/lists"}