{"id":37019861,"url":"https://github.com/kenbot/goggles","last_synced_at":"2026-01-14T02:13:18.384Z","repository":{"id":57720740,"uuid":"79912030","full_name":"kenbot/goggles","owner":"kenbot","description":"Pleasant, yet principled Scala optics DSL","archived":false,"fork":false,"pushed_at":"2020-10-10T04:23:11.000Z","size":169,"stargazers_count":197,"open_issues_count":24,"forks_count":7,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-01-29T03:42:18.018Z","etag":null,"topics":["dsl","functional-programming","lens","monocle","optics","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/kenbot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-01-24T13:03:05.000Z","updated_at":"2023-05-15T11:25:21.000Z","dependencies_parsed_at":"2022-09-02T12:31:16.070Z","dependency_job_id":null,"html_url":"https://github.com/kenbot/goggles","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/kenbot/goggles","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenbot%2Fgoggles","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenbot%2Fgoggles/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenbot%2Fgoggles/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenbot%2Fgoggles/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kenbot","download_url":"https://codeload.github.com/kenbot/goggles/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kenbot%2Fgoggles/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408711,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["dsl","functional-programming","lens","monocle","optics","scala"],"created_at":"2026-01-14T02:13:17.737Z","updated_at":"2026-01-14T02:13:18.374Z","avatar_url":"https://github.com/kenbot.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Goggles - pleasant, yet principled optics DSL\n[![Build Status](https://secure.travis-ci.org/kenbot/goggles.png?branch=master)](http://travis-ci.org/kenbot/goggles)\n\nOptics libraries are either too limited, or too hard to use.\nGoggles builds on Scala's powerful Monocle library, making\nimmutability easy, fun, and boring, like it should be.\n\nYou already know how to use it. \n\n```scala\n\nimport goggles._ \n\ncase class Topping(cherries: Int)\ncase class Cake(toppings: List[Topping])\ncase class Bakery(cakes: List[Cake])\n\nval myBakery = Bakery(List(Cake(List(Topping(0), Topping(3))), \n                           Cake(List(Topping(4))), \n                           Cake(Nil)))\n\nget\"$myBakery.cakes*.toppings[0].cherries\"\n// List(0, 4)\n\nset\"$myBakery.cakes*.toppings[0].cherries\" := 7\n// Bakery(List(Cake(List(Topping(7), Topping(3))), \n//             Cake(List(Topping(7))), \n//             Cake(Nil)))\n\n```\nThe DSL runs in the compiler, and is completely typesafe.\nIt generates plain Monocle code.\n\n## Getting started\nGoggles supports Scala 2.11 and 2.12. Add the following to your `build.sbt` file:\n```scala\nlibraryDependencies ++= Seq(\"com.github.kenbot\" %%  \"goggles-dsl\"     % \"1.0\",\n                            \"com.github.kenbot\" %%  \"goggles-macros\"  % \"1.0\")\n\nscalacOptions += \"-Yrangepos\" // Enables better error messages\n```\n\n## Motivation\n### 1. Functional programming needs optics \nIn imperative programming, a game world might be updated like this:\n```\ngame.currentLevel.player.health += 20\n```\nEven just holding a reference to the player, we can be confident\nthat the change will be seen by everyone observing, without\nknowing anything about the external environment. However,\nmutability conveys a severe penalty in complexity of\nbehaviour, and our human ability to reason about the code.\n\nOn the other hand, naively using immutable structures leads\nto unfortunate problems.\n```\n  game.copy(currentLevel = \n    game.currentLevel.copy(player = \n      game.currentLevel.player.copy(health =\n        game.currentLevel.player.health + 20\n      )\n    )\n  )\n```\nUgly, yes, but it gets worse: recreating the object graph is\na dire failure of modularity. We must now know exactly where\nthe object sits in the world-structure, and how to recreate\nevery detail up to the root. Modularity is supposed to be\na flagship benefit of FP - how embarrassing!\n\n_Optics_ are a family of pure-functional techniques that\nmodel access and mutation with composable abstractions.\nThey are the best answer that has emerged to this\ndilemma; without them FP is dismally unsuited to a\nrange of mundane problems.\n\n\n### 2. Power vs ease-of-use. Why choose?\nThe modifying-immutable-structures problem has been addressed\nin a variety of ways.\n\nDynamic environments such as Clojure, jq, and Javascript have\nfeatures that allow easy manipulation of structures without\nmutation, but in a very constrained, domain-specific context.\n\nHaskell's `Control.Lens` is wonderfully powerful and abstract,\nbut has a reputation for being difficult to learn and use.\nWhy can't we have our cake and eat it too?\n\n### 3. Monocle + Goggles\nMonocle is the leading optics library in Scala; it has a small,\nwell-designed core, but its day-to-day user experience leaves\na little to be desired. It has much of the power of\n`Control.Lens`, and also has much of the conceptual weight\nand learning curve.\n\nThis makes it an ideal core for building an optics DSL.\nGoggles aims to provide an intuitive, discoverable interface\nover Monocle for beginners, while helping experienced users\nget the job done with a minimum of fuss.\n\n## Features\n### Navigate case class-like fields by name\n```scala\nimport goggles._ \n\ncase class City(name: String, population: Int)\ncase class State(name: String, capital: City)\nval state = State(\"Victoria\", City(\"Melbourne\", 4087000))\n\nget\"$state.capital.population\"\n// 4087000\n\nset\"$state.capital.population\" += 1\n// State(\"Victoria\", City(\"Melbourne\", 4087001))\n```\nThe `+=` is syntax sugar; it requires an implicit `scala.Numeric`\n in scope for the result type.\n\n### Interpolate any Monocle optic\n```scala\n\nimport goggles._ \nimport monocle.std.string.stringToInt\nimport monocle.std.int.intToChar\n\nget\"${\"113\"}.$stringToInt.$intToChar\"\n// Some('q')\n\nset\"${\"113\"}.$stringToInt.$intToChar\" ~= (_.toUpper)\n// \"81\" \n```\n\n### Compose Monocle optics\n```scala\nimport goggles._\nimport monocle.std.string.stringToInt\nimport monocle.std.int.intToChar\n\nval myLens = lens\"$stringToInt.$intToChar\"\n// monocle.PPrism[String,String,Char,Char] = monocle.PPrism$$anon$1@2b6b0069\n\nmyLens.getOption(\"113\")\n// Some('q')\n```\n\n### Traverse over collections\n```scala\nimport goggles._\n\ncase class Point(x: Double, y: Double)\nval polygon = List(Point(0.0, 0.0), Point(0.0, 1.0), Point(1.0, 1.0), Point(1.0, 0.0))\nget\"$polygon*.x\"\n// List(0.0, 0.0, 1.0, 1.0)\n\nset\"$polygon*.x\" += 1.5\nList(Point(1.5, 0.0), Point(1.5, 1.0), Point(2.5, 1.0), Point(2.5, 0.0))\n\n```\nAny type for which an implicit `monocle.function.Each` is\nin scope can use `*`\n\n\n### Select optional values\n```scala\nimport goggles._\n\ncase class Estate(farm: Option[Farm])\ncase class Farm(prizeChicken: Option[Chicken])\ncase class Chicken(egg: Option[Egg])\ncase class Egg(weight: Double)\nval estate = Estate(Some(Farm(Some(Chicken(Some(Egg(2.3)))))))\n\nget\"$estate.farm?.prizeChicken?.egg?.weight\"\n// Some(2.3)\n\nset\"$estate.farm?.prizeChicken?.egg?.weight\" *= 2\n// Estate(Some(Farm(Some(Chicken(Some(Egg(4.6)))))))\n\n```\nAny type for which an implicit `monocle.function.Possible` is\nin scope can use `?`\n\n### Select indexed values\n```scala\nimport goggles._\n\nsealed trait Square\ncase object - extends Square\ncase object X extends Square\ncase object O extends Square\n\n\nval ticTac: Vector[Vector[Square]] = \n  Vector(\n    Vector(X, -, -),\n    Vector(O, X, -),\n    Vector(-, O, O))\n\nval i = 0\nget\"$ticTac[$i][0]\"\n// Some(X) \n\nset\"$ticTac[2][0]\" := O\n//  Vector(\n//    Vector(X, -, -),\n//    Vector(O, X, -),\n//    Vector(O, O, O))\n```\nAny type for which an implicit `monocle.function.Index` is\nin scope can use `[i]` with an index.\n\n### Great compilation error messages\nHelpful compiler errors are a first class part of Goggles'\ndesign, hopefully encouraging exploration, clarifying optics\nconcepts and allowing the functionality to be discoverable.\n```\nscala\u003e get\"$myBasket.items*.qty.foo\"\n\u003cconsole\u003e:18: error: Int doesn't have a 'foo' method\n\n Sections  │ Types                         │ Optics\n───────────┼───────────────────────────────┼───────────────────────────\n $myBasket │ ShoppingBasket                │\n .items    │ ShoppingBasket  ⇒  List[Item] │ Lens\n *         │ List[Item]      ⇒  Item       │ Traversal\n .qty      │ Item            ⇒  Int        │ Lens, returning Traversal\n foo       │ Int             ⇒  ???        │\n\n       get\"$myBasket.items*.qty.foo\"\n       ^\n```\n\n\n## Comparison to other approaches\n### Goggles itself\n```scala\nset\"$myBakery.cakes*.toppings[0].cherries\" := 7\n```\n\nGoggles is not an optics library itself; it is only a new user\ninterface built on a subset of Monocle, and interoperates\nseamlessly with the rest. It uses whitebox macros, meaning\nthat the contents of the macro decide the static return type.\n\nGoggles takes the view that macros that base their behaviour\non the structure of code rather than its value are not\nreferentially-transparent, and not consistent with the\nbest traditions of FP. Repurposing Scala's syntax to do\nthings that aren't Scala is surprising to users and\nimposes an unnecessary cognitive burden.\n\nExtensions to `StringContext` form the main mechanism, because:\n* It isn't Scala, and the String clearly demarcates regular\nScala from the designated DSL area.\n* This gives us enormous flexibility to choose the syntax we want.\n\nThere are some disadvantages:\n* There is no IDE support out of the box: it just looks like\na string to IDEs. (Could this be remedied with plugins?)\n* Because interpolated optics get evaluated before the rest\nof the macros, the type inference is poor for arguments.\n\n### [QuickLens](https://github.com/adamw/quicklens)\n```scala\nmodify(myBakery)(_.cakes.each.toppings.at(0).cherries).setTo(7)\n```\n\nQuickLens is designed to be a lightweight alternative to\nMonocle; it is solely focused on manipulating case classes.\nIt uses a fluent API with blackbox macros, which deconstruct\nthe given code tree to discover path information. It supports\nseveral features like \"each\" traversal and indexing, but lacks\nan overarching, cohesive optics model outside of the DSL.\nIn addition, the fluent API supports manipulating several\npoints in the path at once, and Prism-style navigation of\nsum types.\n\n### [Monocle's internal DSL](https://github.com/julien-truffaut/Monocle/blob/master/macro/shared/src/main/scala/monocle/macros/syntax/GenApplyLensSyntax.scala)\n```\nmyBakery.lens(_.cakes)\n```\nMonocle itself contains some internal syntactic helpers,\nincluding a simple DSL for convenient case class manipulation.\nCurrently it uses a blackbox macro to deconstruct a code\ntree, which generates a `monocle.Lens`.\n\n\n### [Shapeless Lenses](https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/lenses.scala)\n```\nlens[Bakery].cakes.get(myBakery)\n```\nShapeless offers Lens and Prisms, which allow automatic\nnavigation of case classes and sum types. It uses Dynamic\nto allow Scala-like syntax, and uses a thicket of typeclasses\nto prove validity. It supports indexing and products, but\nnot traversals. As with many of Shapeless' concepts,\nunderstanding the mechanism used requires a high level\nof proficiency, and the compile errors are quite unhelpful.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkenbot%2Fgoggles","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkenbot%2Fgoggles","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkenbot%2Fgoggles/lists"}