{"id":16696516,"url":"https://github.com/djspiewak/skolems","last_synced_at":"2025-07-21T18:32:09.114Z","repository":{"id":57718354,"uuid":"221793026","full_name":"djspiewak/skolems","owner":"djspiewak","description":"A microlibrary for Scala encodings of higher-rank quantifiers","archived":false,"fork":false,"pushed_at":"2020-04-28T16:12:29.000Z","size":48,"stargazers_count":60,"open_issues_count":2,"forks_count":8,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-10-13T17:43:54.469Z","etag":null,"topics":["encoding","existential","rank-n","scala","types","universal"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/djspiewak.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-11-14T21:50:38.000Z","updated_at":"2024-03-11T21:26:35.000Z","dependencies_parsed_at":"2022-09-14T21:50:27.984Z","dependency_job_id":null,"html_url":"https://github.com/djspiewak/skolems","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/djspiewak%2Fskolems","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/djspiewak%2Fskolems/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/djspiewak%2Fskolems/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/djspiewak%2Fskolems/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/djspiewak","download_url":"https://codeload.github.com/djspiewak/skolems/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221817807,"owners_count":16885633,"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":["encoding","existential","rank-n","scala","types","universal"],"created_at":"2024-10-12T17:43:54.002Z","updated_at":"2024-10-28T10:39:02.612Z","avatar_url":"https://github.com/djspiewak.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Skolems [![Build Status](https://travis-ci.com/djspiewak/skolems.svg?branch=master)](https://travis-ci.com/djspiewak/skolems)\n\nThis library simply contains a few different encodings of universal and existential quantification of types. Whereas in Haskell one might write something like this (with appropriate extensions):\n\n```haskell\ntype Uni f g = forall a . f a -\u003e g a\ntype Exi a = exists b . (b, b -\u003e a)\n```\n\nUnfortunately, Scala does not directly provide support for universally quantified higher-rank types, and its support for existential quantification of higher-rank types is spotty and extremely buggy. This project provides better, more uniform alternatives with sane type inference and sound behavior.\n\nAs a note, in most of the documentation I will be using the symbols ∀ (\"for all\") and ∃ (\"exists\"). Skolems allows you to use these symbols exactly as written, or if you prefer Latin characters, you can use `Forall` and `Exists`. Similarly, kind-projector allows you to use `Lambda` rather than `λ` if you find that easier to read (or type). I also have a habit of using α (lowercase \"alpha\") as a variable in my type lambdas, but this is not in any way required.\n\n## Usage\n\n```sbt\nlibraryDependencies += \"com.codecommit\" %% \"skolems\" % \"\u003cversion\u003e\"\n```\n\nPublished for Scala 2.13, 2.12, and 2.11. This library does not have *any* upstream dependencies (though I would dearly love to add a Cats module at some point, since `Forall` and `Exists` both form useful classes).\n\n## API\n\nJust to conceptualize some of this, it's worth taking a moment to digress on the functionality available in this department with *vanilla* Scala 2.13 (note that Scala 3 improves on this in some important areas, and actually makes it worse in others). Also note that when I say \"vanilla Scala\", I actually mean \"vanilla Scala with [kind-projector](https://github.com/typelevel/kind-projector)\", since it's almost impossible to talk about any of this stuff *without* kind-projector.\n\nIn Scala *today*, what follows is the only way to express universal quantification:\n\n```scala\ndef foo[A](a: A): List[A] = List(a)\n```\n\nThis function is universally quantified over `A`. We often refer to this special case of quantification as *polymorphism*, or sometimes as \"parametricity\" or \"generics\". Whatever word you choose to use, it all comes down to the same thing. Unfortunately, this functionality in the language has a very important limitation: you cannot write it *as a value*. As an example:\n\n```scala\ndef foo[A](a: A): List[A] = List(a)\nval otherFoo = foo(_)    // ?????\n```\n\nThat doesn't work. Or rather, it works in the sense that it compiles, but the resulting `otherFoo` will have the very unhelpful `Nothing =\u003e List[Nothing]` type signature, which is not at all the type that `foo` has. This is because the quantification of `foo` is lost when it is expressed as a free value, which is why this form of quantification (as present in Scala) is sometimes referred to as *let-bound polymorphism*, since the polymorphism is lost if you detach the value (which is to say, the function) from its declaration.\n\nNow, let-bound polymorphism is quite useful, but it is simply insufficient for many cases. As a trivial example, imagine we wanted to write a function that takes two parameters, one `String` and one `Int`, and *also* takes a function with the same theoretical type signature as `foo` above, and then applies it to both. So in other words, callers of the function would pass their `String` and `Int` values, as well as `foo` (or something with the same type as `foo`), and this function would apply the `foo`-like thing to both types.\n\nWe can't write this today in Scala. Writing it in Haskell would look something like this:\n\n```haskell\nbippy :: String -\u003e Int -\u003e (forall a . a -\u003e [a]) -\u003e ([String], [Int])\nbippy s i f = (f s, f i)\n```\n\nThat's actually... really straightforward, but you notice the trick: Haskell is allowing us to write the type of a *value* which itself is universally quantified. In other words, in Haskell, `foo` isn't stuck being a `def`, it is actually a value just like anything else.\n\nSkolems allows you to write this function in Scala. It looks like this:\n\n```scala\nimport skolems.∀\n\ndef bippy(s: String, i: Int, f: ∀[λ[α =\u003e α =\u003e List[α]]]): (List[String], List[Int]) =\n  (f[String](s), f[Int](i))\n```\n\nOr, if you prefer the Latin character version:\n\n```scala\nimport skolems.Forall\n\ndef bippy(\n    s: String,\n    i: Int,\n    f: Forall[Lambda[a =\u003e a =\u003e List[a]]])\n    : (List[String], List[Int]) =\n  (f[String](s), f[Int](i))\n```\n\n*Calling* this function is relatively straightforward as well:\n\n```scala\ndef foo[A](a: A): List[A] = List(a)\n\nbippy(\"hi\", 42, ∀[λ[α =\u003e α =\u003e List[α]]](foo))   // =\u003e (List(\"hi\"), List(42))\n```\n\nIn some cases, it may be cleaner to use a named type alias rather than the type lambda:\n\n```scala\ndef foo[A](a: A): List[A] = List(a)\n\ntype ListConstr[A] = A =\u003e List[A]\nbippy(\"hi\", 42, ∀[ListConstr](foo))   // =\u003e (List(\"hi\"), List(42))\n```\n\nSo far, we've only looked at universal quantification, but what about existential? Here, Scala has, on paper, slightly better support in the form of the `forSome` type operator:\n\n```scala\ndef foo(unapplied: (A, A =\u003e String) forSome { type A }): String = {\n  val (v, f) = unapplied\n  f(v)\n}\n\nfoo((42, (i: Int) =\u003e i.toString))\nfoo((\"hi\", (s: String) =\u003e s))\n```\n\nOr, more interestingly:\n\n```scala\ndef foo(unapplied: List[(A, A =\u003e String) forSome { type A }]): List[String] =\n  unapplied map {\n    case (v, f) =\u003e f(v)\n  }\n\nfoo(\n  List(\n    (42, (i: Int) =\u003e i.toString),\n    (\"hi\", (s: String) =\u003e s)))\n```\n\nUnfortunately, `forSome` is extremely brittle. The compiler will often \"lose\" the quantifier as it passes through various transformations in more complex types. Additionally, there are certain cases where the compiler will do straight-up unsound things with the type. For example, I've seen scalac convert the following types into one another (or rather, more complex variants of these types):\n\n```scala\ntype One = (F[A] forSome { type A }) =\u003e B\ntype Two = (F[A] =\u003e B) forSome { type A }\n```\n\nThese are not the same type! Not even close. In fact, `One` and `Two` could *actually* be rewritten as the following:\n\n```scala\ntype Neo = ∀[λ[α =\u003e F[α] =\u003e B]]\ntype Wot = ∀[F] =\u003e B\n```\n\nIn other words, pretending that `One` and `Two` are the same type (which, as I said, I've seen the compiler do at times when using `forSome`) is not just limiting, it's actually wrong and can generate type unsoundness. tldr, `forSome` is buggy in Scala, and you should try not to use it.\n\nBut of course, if you *don't* use `forSome`, then your only option for encoding existential quantification is the ad-hoc approach using type members. For example:\n\n```scala\ntrait Unapplied {\n  type A\n  val a: A\n  def apply(v: A): String\n}\n\ndef foo(unapplied: List[Unapplied]): List[String] =\n  unapplied map { u =\u003e\n    u(u.v)\n  }\n\nfoo(\n  List(\n    new Unapplied {\n      type A = Int\n      val a = 42\n      def apply(i: Int): String = i.toString\n    },\n    new Unapplied {\n      type A = String\n      val a = \"hi\"\n      def apply(s: String): String = s\n    }))\n```\n\nThis works a lot better than the `forSome` approach, and the Scala compiler is *much* better about not losing its mind when using this encoding, but it's a little awkward to create a new wrapper type every time you need an existential. Also it's unbelievably verbose.\n\nFor this reason, Skolems introduces the `Exists` (`∃`) type, which is analogous to `Forall` (`∀`) except for existential rather than universal types.\n\n```scala\nimport skolems._\n\ndef foo(unapplied: List[∃[λ[α =\u003e (α, α =\u003e String)]]]): List[String] =\n  unapplied map { u =\u003e\n    val (v, f) = u.value\n    f(v)\n  }\n\nfoo(\n  List(\n    ∃[λ[α =\u003e (α, α =\u003e String)]]((42, (i: Int) =\u003e i.toString)),\n    ∃[λ[α =\u003e (α, α =\u003e String)]]((\"hi\", (s: String) =\u003e s))))\n```\n\nBasically, think of `Exists` as a \"better `forSome`\". It will type infer sanely and it's almost exactly as easy to use.\n\n### Implicit Evidence\n\nAnother interesting use of higher-rank universal types is in defining polymorphic implicit values. As an example, [Cats](https://typelevel.org/cats) defines an instance of `Monad` for `Either[A, ?]`, for *any* type `A`. This is defined in the following way:\n\n```scala\nimplicit def eitherMonad[A]: Monad[Either[A, ?]] = ???\n```\n\nHere again we see let-bound polymorphism rearing its ugly head. While it is possible to *define* such a value (a monad for `Either` for all type instantiations), it isn't possible to take it as a parameter. For example:\n\n```scala\n// we can write this function!\ndef foo[F[_], A](fa: F[A])(implicit F: Monad[F]) = ???\n\n// but writing this function is awkward\ndef bar[F[_, _], A, B, C](\n    one: F[A, C],\n    two: F[B, C])(\n    implicit F1: Monad[F[A, ?]],\n    F2: Monad[F[B, ?]]) = ???\n```\n\nNotice how we had to take two `Monad` instances (`F1` and `F2`) *for the same type constructor*, which is more than a little weird when the instances are actually *the same* instance: `eitherMonad`. There are other cases where this problem can get even worse, or even intractable.\n\nWhat we really want to write is this:\n\n```scala\ndef bar[F[_, _], A, B, C](\n    one: F[A, C],\n    two: F[B, C])(\n    implicit F: ∀[λ[α =\u003e Monad[F[α, ?]]]]) = ???\n```\n\nIn other words, a *universally quantified* version of the `Monad` which has the same expressivity as the original definition, `eitherMonad`.\n\nWith Skolems, you can do exactly this. In fact, the above works exactly as written, and you can even call it in exactly the way you expect!\n\n```scala\nbar[Either, String, Boolean, Int](Right(42), Left(false))\n```\n\nAnd that's it. It just works. In fact, you can even omit the type parameters:\n\n```scala\nbar(Right(42), Left(false))\n```\n\nThat works too.\n\nThe *reason* this works is Skolems is able to materialize an implicit `Forall` type *given* an implicit definition that uses let-bound polymorphism (such as `eitherMonad`). Unfortunately, it cannot *currently* go in the other direction. In other words:\n\n```scala\ndef foo[F[_, _]](implicit F: ∀[λ[α =\u003e Monad[F[α, ?]]]]) = Monad[F[String, ?]]  // nope!\n```\n\nThis doesn't work. **Yet.** The problem is that scalac lacks a particular form of symbolic unification in its type checker. This isn't really a *theoretical* problem, scalac's implementation just can't handle the kind of equation that is necessary to typecheck this. Skolems will add support for this using a macro in an upcoming release.\n\nBut until then... you can materialize the instance manually:\n\n```scala\ndef foo[F[_, _]](implicit F: ∀[λ[α =\u003e Monad[F[α, ?]]]]) = {\n  implicit val fs = F[String]\n  implicit val fi = F[Int]\n\n  Monad[F[String, ?]]  // no problem!\n  Monad[F[Int, ?]]     // also cool\n}\n```\n\nThat works fine for a lot of cases, and we'll be making things even better soon!\n\nAs an aside, the same implicit materialization *should* work for existentially-quantified implicit declarations just the same as it works for universally-quantified ones, but I haven't been able to come up with a good motivating example for this.\n\n### Identities\n\nThere are a number of identities which hold for rank-n quantification in first-order logic (which is to say, Scala's type system). These identities are very difficult to access and highly opaque when using let-bound polymorphism and `forSome`, but Skolems can make them very easy and direct. These identities are specifically as follows (with their corresponding implementation in the API):\n\n- ![](https://i.imgur.com/MJvkK36.png)\n  + Left-to-right: `Forall.raise`\n  + Right-to-left: `Forall.lower`\n    * Also `Forall.lowerA` if the let-bound polymorphic quantifier encoding is more useful than the `Forall`-based version\n- ![](https://i.imgur.com/kvNiR39.png)\n  + Left-to-right: `Exists.raise`\n  + Right-to-left: `Exists.lower`\n    * Also `Exists.lowerE` if the `forSome` quantifier encoding is more useful than the `Exists`-based version\n\nAs a note on the naming convention, you should think of `raise` as \"raise the rank\". In other words, you're going from a rank-1 type (with the quantifier on the outside) to a rank-2 type (with the quantifier on the inside). Obviously, `lower` is the inverse. These functions are relatively trivial to define (except for `Exists.lower`, which requires an `asInstanceOf` due to the fact that Scala only has local type inference), but it's nice to have them already available.\n\n## Related Work\n\nThe encodings in this project occurred in their (to my knowledge) original forms in Scalaz. The use of the inexpressible sentinel type as a mechanism for inferring a rank-2 universal from a Scala expression has varied origins. Miles Sabin mentioned the trick to me a few years ago and had some (now discarded) prototype constructs in Shapeless which took advantage of it. Ed Kmett has also discussed it as an encoding which infers better, though I cannot currently find a link to his use of it, and based on my memory of what he was doing, it may have been unsound all along. Scalaz's `Forall` has a peculiar `apply` function which uses a doubly-negated existential type (expressed using `forSome` and `A =\u003e Nothing`) to infer a rank-2 universal. This trick was *also* devised originally by Miles Sabin (waaaaaay back in the mists of time), and if you look very very closely, it's really just yet another way of expressing the \"inexpressible sentinel type\" idea. As for the idea of using an abstract type member within an object to encode the sentinel, I first learned of this from Kai, though Alex Konovalov also uses this extensively in several of his projects and I honestly don't know where it originates.\n\nSpeaking of Alex, he has a really interesting library called [polymorphic](https://github.com/alexknvl/polymorphic) which may be a better use of your time than my failings here! Some differences between polymorphic and skolems:\n\n- His `Forall` and `Exists` are entirely unboxed\n- He provides a very clever `Instance` constructor for the common implicit case of existential pairs\n- He also provides `Pi` and `Sigma` for dependent type quantification\n- Polymorphic depends on cats-core, while Skolems has no dependencies (for better or worse)\n- Skolems defines a mechanism for materializing rank-n implicit values, while this is not provided by polymorphic in any form except `Instance`\n- The type inference seems to be nicer with polymorphic than with skolems\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdjspiewak%2Fskolems","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdjspiewak%2Fskolems","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdjspiewak%2Fskolems/lists"}