{"id":17597287,"url":"https://github.com/battermann/pureapp","last_synced_at":"2026-03-01T01:02:56.653Z","repository":{"id":86688947,"uuid":"122790003","full_name":"battermann/pureapp","owner":"battermann","description":"A small library for writing referentially transparent and stack-safe sequential programs","archived":false,"fork":false,"pushed_at":"2018-10-27T13:29:53.000Z","size":55,"stargazers_count":40,"open_issues_count":0,"forks_count":3,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-30T06:46:24.873Z","etag":null,"topics":["console-application","elm-architecture","referentially-transparent","scala"],"latest_commit_sha":null,"homepage":"http://blog.leifbattermann.de/2018/04/29/interactive-command-line-applications-in-scala-well-structured-and-purely-functional/","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/battermann.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-02-24T23:44:35.000Z","updated_at":"2024-10-24T04:35:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"2c348c74-426d-4931-b851-40fe410ad817","html_url":"https://github.com/battermann/pureapp","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/battermann/pureapp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/battermann%2Fpureapp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/battermann%2Fpureapp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/battermann%2Fpureapp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/battermann%2Fpureapp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/battermann","download_url":"https://codeload.github.com/battermann/pureapp/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/battermann%2Fpureapp/sbom","scorecard":{"id":227247,"data":{"date":"2025-08-11","repo":{"name":"github.com/battermann/pureapp","commit":"2775379f0d5e6632aeddeea31a66f7c0320850a8"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":0,"reason":"Found 0/27 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 5 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-17T04:02:53.539Z","repository_id":86688947,"created_at":"2025-08-17T04:02:53.539Z","updated_at":"2025-08-17T04:02:53.539Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29957128,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-28T22:53:01.873Z","status":"ssl_error","status_checked_at":"2026-02-28T22:52:50.699Z","response_time":90,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["console-application","elm-architecture","referentially-transparent","scala"],"created_at":"2024-10-22T09:24:43.803Z","updated_at":"2026-03-01T01:02:56.570Z","avatar_url":"https://github.com/battermann.png","language":"Scala","readme":"# PureApp\n\nA principled and opinionated library for writing purely functional, easy to reason about, and stack-safe sequential programs partly inspired by [Elm](http://elm-lang.org/), [scalm](https://github.com/julienrf/scalm), and scalaz's [SafeApp](https://github.com/scalaz/scalaz/blob/bffbbcf366ca3a33dad6b3c10683228b20812bcf/effect/src/main/scala/scalaz/effect/SafeApp.scala)\n\n## installtion\n\n    libraryDependencies += \"com.github.battermann\" %% \"pureapp\" % \"0.6.0\"\n\n## overview\n\nThe architecture for PureApp applications is mainly inspired by the [Elm Architecture](https://guide.elm-lang.org/architecture/).\n\nAn Idiomatic PureApp program is completely pure and referentially transparent.\n\nIt can be either implemented as the main application or it can be composed of other PureApp programs (see below).\n\nA program consists of three components:\n\n### model\n\nThe model represents the immutable application state.\n\n### update\n\nA way to update the application's state. `update` is a function that takes a `Model` (the current application state) and a `Msg` and returns a new `Model` (a new application state).\n\n### io\n\n`io` is a function that describes all side effects of an application.\n\nUnlike Elm and scalm, PureApp applications do not have a `view` function. Instead `io` is responsible for printing and reading from the standard input/output as well as for other side effects.\n\n`io` takes a `Model` and returns an `F[Msg]`. Where `F[_]` has an instance of [`Effect[F]`](https://typelevel.org/cats-effect/typeclasses/effect.html). Additionally you can pass immutable, pure values of type `Cmd` that represent commands to perform other side effects than just printing and reading.\n\nInternally the `Msg` that is returned from `io` and wrapped inside an `F[_]` together with the current `Model` is fed back into the `update` function. However, this is hidden from the user and we do not have to worry about this.\n\n## termination\n\nTo control when to terminate a PureApp application we define `def quit(msg: Msg): Boolean`. If `quit` returns `true` when applied to a `Msg` coming from the `io` function the program will terminate.\n\n## example\n\nHow to use PureApp can best be demonstrated with an example. Here is the PureApp version of the [Elm counter example](http://elm-lang.org/examples/buttons):\n\n```scala\nimport com.github.battermann.pureapp._\n// import com.github.battermann.pureapp._\n\nimport com.github.battermann.pureapp.interpreters.Terminal._\n// import com.github.battermann.pureapp.interpreters.Terminal._\n\nimport cats.effect.IO\n// import cats.effect.IO\n\nobject Main extends SimplePureApp[IO] {\n\n  // MODEL\n\n  type Model = Int\n\n  sealed trait Msg\n  case object Increment    extends Msg\n  case object Decrement    extends Msg\n  case object InvalidInput extends Msg\n  case object Quit         extends Msg\n\n  def init: Model = 42\n\n  def quit(msg: Msg): Boolean = msg == Quit\n\n  // UPDATE\n\n  def update(msg: Msg, model: Model): Model =\n    msg match {\n      case Increment    =\u003e model + 1\n      case Decrement    =\u003e model - 1\n      case Quit         =\u003e model\n      case InvalidInput =\u003e model\n    }\n\n  // IO\n\n  def io(model: Model): IO[Msg] =\n    for {\n      _     \u003c- putStrLn(model.toString)\n      _     \u003c- putStr(\"enter: +, -, or q\u003e \")\n      input \u003c- readLine\n    } yield {\n      input match {\n        case \"+\" =\u003e Increment\n        case \"-\" =\u003e Decrement\n        case \"q\" =\u003e Quit\n        case _   =\u003e InvalidInput\n      }\n    }\n}\n// defined object Main\n```\n\n## three different patterns\n\nPureApp supports three different patterns:\n\n### SimplePureApp\n\nA simple program (like the counter example from above) knows only models and messages. We can create a simple program by extending from the `SimplePureApp[F_]]` class.\n\n### StandardPureApp\n\nA *standard* program which extends `StandardPureApp[F[_]]` also supports commands. Normally printing to and reading from the console can be done based on the `Model` (the application state). If we want to perform other side effecting actions, we often can't or don't want to do this based on the application state. Instead we can use commands that represent requests for performing such tasks. The `io` function then becomes the interpreter for our commands as [this example](examples/command/src/main/scala/example/Main.scala) demonstrates.\n\n### PureApp\n\nA program that can create and dispose resources in a referentially transparent way has to extend the `PureApp[F[_]]` class. The type `Resource` represents an environment containing disposable resources and other things that do not belong into the domain model (like e.g. a configuration). We have to provide an implementation for `def acquire: F[Resource]` and we can override  `def dispose(resource: Resource): F[Unit]` to dispose resources.\n\nThe `io` function of an `PureApp ` provides an additional parameter of type `PureApp ` that we can now use while interpreting our commands. [Here is an example](examples/env/src/main/scala/Main.scala) uses an HTTP client as a resource.\n\n\n## minimal working skeleton\n\nTo create a minimal working skeleton the main object of an application has to extend one of the three abstract classes mentioned above:\n\n- `SimplePureApp[F[_]]`\n- `StandardPureApp[F[_]]`\n- or `PureApp[F[_]]`\n\nThen the types `Model` and `Msg` have to be defined. Depending on which pattern we use we might have to define `Cmd` and `Resource` as well.\n\nUsually `Msg` and `Cmd` will be implemented as sum types.\n\nFinally all abstract methods have to be implemented:\n\n- `init`\n- `update`\n- `io`\n- `quit` (if we want the program to terminate)\n\nAnd optionally:\n\n- `acquire`\n- `dispose`\n\nHere is a minimal working skeleton to get started:\n\n```scala\nobject Main extends StandardPureApp[IO] {\n\n  // MODEL\n\n  type Model = String\n\n  type Msg = Unit\n\n  type Cmd = Unit\n\n  def init: (Model, Cmd) = (\"Hello PureApp!\", ())\n\n  def quit(msg: Msg): Boolean = true\n\n  // UPDATE\n\n  def update(msg: Msg, model: Model): (Model, Cmd) = (model, ())\n\n  // IO\n\n  def io(model: Model, cmd: Cmd): IO[Msg] =\n    putStrLn(model)\n}\n// defined object Main\n\nMain.main(Array())\n// Hello PureApp!\n```\n\nAn example that is a little more involved can be found here: [TodoList](https://github.com/battermann/pureapp/blob/master/examples/todolist/src/main/scala/example/Main.scala).\n\n## command line args\n\nTo use command line arguments we have to override the `runl(args: List[String])` method. And the call `run(_init: (Model, Cmd))` manually. Now we can use `args` for creating the initial `Model` and `Cmd` e.g. like this:\n\n```scala\nobject Main extends StandardPureApp[IO] {\n  \n  override def runl(args: List[String]) =\n\t  run((Model(args = args), Cmd.Empty))\n\t\n  // ...\n}\n```\n\n## composability\n\nPureApp programs are pure, immutable values represented by the case class `Program[F[_]: Effect, Model, Msg, Cmd, Resource, A]`.\n\nThere are different constructors for the three different flavours described above:\n\n- `Program.simple(...)`\n- `Program.standard(...)`\n- or `Program.apply(...)`\n\nBy default, the final result of a program is `F[Model]`, the final application state. If we need our program to return something else we can map over it with `map` and pass a function `f: A =\u003e B`. \n\nTo finally create a composable program, we have to transform it to it's representation in the context of it's effect type `F[_]` by calling `build()`. Note that this will not run the program.\n\nNow we have all the compositional capabilities at hand that the type `F[_]` offers.\n\nHere is a (not very meaningful) example of showing the technique of composing programs:\n\n```scala\nimport cats.implicits._\n// import cats.implicits._\n\nval p1 = Program.simple(\n\t  \"Hello PureApp 1!\",\n\t  (_: Unit, model: String) =\u003e model,\n\t  (_: String) =\u003e IO.unit,\n\t  (_: Unit) =\u003e true\n  ).map(List(_)).build()\n// p1: cats.effect.IO[List[String]] = \u003cfunction1\u003e\n\nval p2 = Program.simple(\n\t  \"Hello PureApp 2!\",\n\t  (_: Unit, model: String) =\u003e model,\n\t  (_: String) =\u003e IO.unit,\n\t  (_: Unit) =\u003e true\n  ).map(List(_)).build()\n// p2: cats.effect.IO[List[String]] = \u003cfunction1\u003e\n\nval program = p1 |+| p2\n// program: cats.effect.IO[List[String]] = IO$378649170\n\nprogram.unsafeRunSync()\n// res1: List[String] = List(Hello PureApp 1!, Hello PureApp 2!)\n```\n\nAlternatively and for convenience, instead of using the constructors we can implement one of the three abstract classes:\n\n- `SimplePureProgram[F_]`\n- `StandardPureProgram[F_]`\n- or `PureProgram[F_]`\n\nHere is how to apply this approach to the example from above:\n\n```scala\nobject Hello1 extends SimplePureProgram[IO] {\n  type Model = String\n  type Msg = Unit\n  def init: Model = \"Hello PureApp 1!\"\n  def quit(msg: Msg): Boolean = true\n  def update(msg: Msg, model: Model): Model = model\n  def io(model: Model): IO[Msg] = IO.unit\n}\n// defined object Hello1\n\nobject Hello2 extends SimplePureProgram[IO] {\n  type Model = String\n  type Msg = Unit\n  def init: Model = \"Hello PureApp 2!\"\n  def quit(msg: Msg): Boolean = true\n  def update(msg: Msg, model: Model): Model = model\n  def io(model: Model): IO[Msg] = IO.unit\n}\n// defined object Hello2\n```\n\nSimilar to scalaz, PureApp offers an abstract class `SafeApp[F[_]]` that provides an implementation of the `main` method by running a specified `Effect[F]`. We can use this to embed the composition of the two programs:\n\n```scala\nobject Main extends SafeApp[IO] {\n\n  val program =\n    Hello1.program.map(List(_)).build() |+|\n      Hello2.program.map(List(_)).build()  \n      \n  override def run: IO[Unit] =\n    program.flatMap(v =\u003e putStrLn(v.toString))\n}\n// defined object Main\n\nMain.main(Array())\n// List(Hello PureApp 1!, Hello PureApp 2!)\n```\n\n\n## internals\n\nInternally PureApp uses an instance of `StateT[F, (Model, Cmd, Resource), Msg]`. The program loop is implemented with `iterateUntil` which is stack safe. And the state is run with the initial `Model` and `Cmd`.\n\nAlso we do not have to run our program. This is handled internally. The given effect is evaluated in the context of `F[_]` to an `IO[Unit]`. Which is then run with `unsafeRunSync` similar to scalaz's SafeApp.\n\n## contributions\n\nI'm happy for any kind of contributions whatsoever, be it comments, issues, or pull requests.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbattermann%2Fpureapp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbattermann%2Fpureapp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbattermann%2Fpureapp/lists"}