{"id":22636118,"url":"https://github.com/47degrees/fetchless","last_synced_at":"2025-04-11T21:11:41.084Z","repository":{"id":58156929,"uuid":"453562995","full_name":"47degrees/fetchless","owner":"47degrees","description":"Port of 47deg/fetch to Tagless Final Style™️","archived":false,"fork":false,"pushed_at":"2025-04-11T05:16:01.000Z","size":445,"stargazers_count":11,"open_issues_count":5,"forks_count":0,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-04-11T21:11:38.602Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/47degrees.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":"AUTHORS.md","dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-30T01:41:39.000Z","updated_at":"2025-04-11T05:16:03.000Z","dependencies_parsed_at":"2023-12-15T06:28:05.635Z","dependency_job_id":"9e5a0e5d-85a8-4211-8144-f7519e7bd7d2","html_url":"https://github.com/47degrees/fetchless","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/47degrees%2Ffetchless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/47degrees%2Ffetchless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/47degrees%2Ffetchless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/47degrees%2Ffetchless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/47degrees","download_url":"https://codeload.github.com/47degrees/fetchless/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248480431,"owners_count":21110937,"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":[],"created_at":"2024-12-09T03:18:21.010Z","updated_at":"2025-04-11T21:11:41.058Z","avatar_url":"https://github.com/47degrees.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# fetchless (proof of concept)\nA port of the Fetch library to Tagless Final style\n\n## Key Differences\n* Encoded in tagless final style first, instead of as a data structure\n* No need for explicit support for logging, timing, rate-limiting, timeouts, etc. as those can use your base effect type.\n* Less runtime overhead due to being implemented in terms of existing effects, rather than as its own effect.\n* Potentially orders of magnitude faster than original Fetch\n* Core code is very small, most code is actually just implicit syntax wrappers for collections \u0026 tuples.\n* No required type class dependencies to call fetching methods (after creating a `Fetch` instance)\n\n## Motivation\nIn Fetch 3.0.0, I made a decision to remove the guarantee for implicit batching on `sequence`/`traverse` calls due to the behavior breaking from a Cats version upgrade.\nThis is reverted as of 3.1.0, but it seemed best at the time with what limited perspective I had that maybe we should instead introduce explicit batching syntax.\nIn hindsight, I think this makes Fetch somewhat less useful and have considered the alternatives.\n\nThe entire point of Fetch, in a sense, is to auto-batch and parallelize requests wherever possible.\nIt does this by encoding a DSL for fetches as an explicit effect type that is interpreted later, using a custom `Monad` instance with overrides on certain applicative methods.\nThere are some pros and cons with this approach:\n\nPros:\n* You don't have to explicitly call `Parallel` methods such as `parTupled`/`parTraverse`/`parSequence` in order to invoke concurrency and batching, and can just use `.tupled`/`.traverse`/`.sequence` as normal.\n* You can treat it as an effect type on its own, and use it in any code that depends on the `Monad` type class.\n\nCons:\n* The abstraction breaks down slightly in assuming that users will know and understand that `flatMap` is strictly sequential, whereas `tupled`/`traverse` might not be. So to optimize fetches, the user needs to be aware of whether or not these `Applicative` methods can be used instead of monadic ones, and in that case it could be argued that the user might as well just explicitly ask for batching in those cases (since those functions require an applicative-friendly signature anyhow)\n* Requires custom code for features such as logging, timing, timeouts, etc. for any intermediate fetch.\n* Must be interpreted at runtime from its reified form, which also implies leaking the `Concurrent` implementation detail for when you plug in an effect type.\n* Can break at a moment's notice if any implementation details in Cats that this depends on for auto-batching support lawfully change (as did happen before 3.0.0)\n\nAs listed above in the \"key differences\" section, a tagless approach has numerous advantages.\nThe two biggest advantages are speed and API flexibility.\n\n### Speed\nWhile not properly benchmarked extensively yet (I wrote this up very quickly and will expand on this later), I've ran some local benchmarks that confirm my initial suspicions about the speed of Fetch vs fetchless.\nOn a fundamental level, it should make sense that a \"Free\"-style DSL is slower to at least some degree than a \"tagless final\"-style one.\nThis is because of how they are encoded, since the tagless version is much more direct and has less overhead by definition.\n\nHere is some code from a (currently local, unpublished) benchmark that should get the point across (time in nanoseconds)\n\n(average, in nanoseconds, over 40 runs)\n\n```\nImmediate fetch traverse result\n1.12204425E7\nImmediate deduped fetch traverse result\n1.58746375E7\nLazyRequest traverse result\n5.33710375E7\nLazyBatchRequest set result\n6.621083E7\nLazyBatchRequest traverse result\n1.01090145E8\nLazyRequest parTraverse result\n1.54424665E8\n```\n\nDoing an identical benchmark on Fetch, looks like the following:\n\n(average, in nanoseconds, over 40 runs)\n```\nFetch traverse result\n7.6349248325E9\n```\n\nIn each case, the benchmark involves traversing over a list of 50,000 integers and performing a fetch, except for `LazyBatchRequest set` which is an explicit batch and not a traversal.\nFor the `immediate` case up top, that is for a direct fetch with no deduping or auto-batching support.\n`LazyRequest` supports deduping and sequencing, but not auto-batching, and `LazyBatchRequest` is an applicative type that supports batches only.\nSo in the absolute worst case for fetchless, it appears that starting with `LazyRequest` and using `parTraverse` to re-encode as a single `LazyBatchRequest` takes well over an order of magnitude less time to perform.\nIt's still possible that future changes will bridge this gap slowly, as more features and functionality are added or possible bugs are fixed, but this is a very promising start and shows that even if you choose the slowest possible fetch option in fetchless, it is still faster than Fetch.\n\n### Flexibility\nThere are a couple details in Fetch that make it not very flexible in the API department:\n\n* `DataSource` instances must contain a `Concurrent` instance for the effect type, even if it is not used in an implementation, simply because it is possible that it could be.\n* It must also specify an execution style and a maximum count for batches, if any. These feel like leaking implementation details that could just be closed over in the implementation.\n* `Fetch.run` and similar require the `Concurrent` type class among others, which means that if you are several layers deep into your program you have to depend on this rather heavy type class for your effect type rather than something more simple like `Monad`.\n* Because a `Fetch` is its own effect, implementing features that happen between sequenced fetches requires implementing the functionality inside the library, rather than providing some kind of way for users to hook in and interleve effects.\n\nfetchless solves this by making the following decisions:\n\n* A `Fetch` instance (similar to `DataSource`) has only two main methods: `single` and `batch`, and no dependency on any specific type class.\n* The `Fetch` instance itself should decide if parallelism or maximum-batch-sizes should be considered in cases of fetching multiple IDs in a batch.\n* The paradigm becomes less about \"invisibly batching fetches\", and more about describing your data models as \"fetchable\" by providing a `Fetch` instance for them. Given a `Fetch` instance exists for some type in the current scope, you can use `.fetch`/`.fetchAll`/`.fetchTupled`/`.fetchLazy` syntax on any ID value or collection.\n* For all additional functionality, we can use the base effect system, reducing maintenance burden as well as increasing efficiency.\n\n## Examples\n\nTo demonstrate the way this works, consider the following example code:\n\n```scala\nimport cats.effect.IO\nimport cats.syntax.all._\nimport fetch.DataSource\nimport fetch.{Fetch =\u003e OGFetch}\n\n//Given an Int, returns it back\nval testDataSource = new DataSource[IO, Int, Int] {\n  def data: Data[Int, Int] = new Data[Int, Int] {\n    def name: String = \"TestDataSource\"\n  }\n\n  implicit def CF: Concurrent[IO] = Concurrent[IO]\n\n  def fetch(id: Int): IO[Option[Int]] = IO.pure(Some(id))\n\n  override def batch(ids: NonEmptyList[Int]): IO[Map[Int, Int]] =\n    IO.pure(ids.toList.map(i =\u003e i -\u003e i).toMap)\n\n}\n\nval testProgram: IO[List[Int]] = OGFetch.run(List(1, 2, 3).map(i =\u003e Fetch(i, testDataSource)).sequence)\n```\n\nFor fetchless, the equivalent code would look like this:\n\n```scala\nimport cats.effect.IO\nimport cats.syntax.all._\nimport fetchless.Fetch\nimport fetchless.syntax._\n\n//Constructor for Fetch instances that will run all batches as single fetches in a strict linear sequence.\nimplicit val testFetch: Fetch[IO, Int, Int] = Fetch.singleSequenced(i =\u003e IO.pure(i))\n\nval testProgram: IO[Map[Int, Int]] = List(1, 2, 3).fetchAll\n```\n\nThere are a couple minor differences in the API type signatures, but the general idea is the same.\nYou can fetch single items with one ID, and you can request multiple IDs at once in a batch.\nThe actual implementation of batches is entirely contained in the `Fetch` instance you create, so you no longer have to\nspecify if you want to run fetches in parallel or in a linear sequence.\nAs long as you are fetching more than one ID at a time, it is guaranteed to use whatever behavior the batch implementation of your `Fetch` instance is designed to do.\n\nHere are the current constructors for `Fetch` instances:\n\n* `singleSequenced` - Only runs single fetches, and batches are ran as sequential. Requires `Applicative`.\n* `singleParallel` - Runs batches in parallel. Requires `Applicative` and `Parallel`.\n* `batchable` - Allows you to specify your own functions for `single` and `batch` fetches. No type class constraints (assumed the user will have their own).\n* `batchOnly` - Runs batches only, and encodes single fetches as a one-element batch. Requires `Applicative`. Whether or not batches are in parallel depends on the user's supplied function for the batch.\n* `const` - A constant `Fetch` that retrieves from an in-memory map.\n* `echo` - A `Fetch` that returns the input the user provides, great for tests.\n\nAnd of course you can always create your own instance, though `batchable` winds up being the same thing with less boilerplate.\n\nTo get around the problem of how to do auto-batching, the solution is to simply defer to the current `Fetch` implementation that the user wires in.\nThe important details should be just that the user is requesting data, either one-at-a-time or in a batch.\nExactly how that batch is acquired, just like the original Fetch library, is unimportant at the point it is being used.\nThe user does not select `traverse` vs `parTraverse`, and so on, but instead they can use the following syntax:\n\n```scala\nval exampleId = 5\n\nval singleFetch: IO[Option[Int]] = 5.fetch[Id, Int, Int] //Can also manually supply the Fetch instance\n\nval effectfulFetch: IO[Option[Int]] = IO(5).fetch[Int]\n\nval traverseFetch: IO[Map[Int, Int]] = List(5).fetchAll //Also configured to work on tuples\n\nval tupledFetch: IO[(Option[Int], Option[Int], Option[Int])] = (5, 5, 5).fetchTupled\n```\n\nThis also side-steps the problem of needing to provide explicit support for popular user-requested features, since all fetches are always directly implemented in terms of `F[_]` and can be manipulated with whatever libraries and combinators users already use (including whatever is in the CE3 standard library)\n\n## DedupedRequest\n\nOne feature not supported in a standard fetch is deduping, since that requires some kind of context to pass around.\nI've added a type `DedupedRequest` and operators to fetch using this provided context, so that in any context you can make very efficient fetches that depend on previous fetches for caching and deduplication.\n\n## LazyRequest and LazyBatchRequest\n\nThe `Fetch` instance can not only deduplicate fetches with `DedupedRequest` but it can also create lazy fetches that approximate the behaavior found in the original `Fetch` library with regards to not only deduplication, but also automatic batching.\n\nA `LazyRequest` is created when you call `.fetchLazy` on an ID, or call `.singleLazy`/`.batchLazy` on a `Fetch` instance.\nIt is essentially just a wrapper type that encodes the intention to chain fetch calls together, so you can `flatMap` multiples of them in sequence and raise effects into its context with `LazyRequest.liftF`/`LazyBatchRequest.liftF`.\n\n`LazyBatchRequest` is the parallel of `LazyRequest` which is not monadic, but only `Applicative` and represents the intention to group multiple fetches into a batch.\nYou can combine them together with `.tupled` and `.traverse`-style methods that work for any `Applicative` and it will guarantee to put all of your requests into a single batch per-fetch-instance.\nHowever, you cannot chain them together, so you need to convert back and forth between `LazyRequest` and `LazyBatchRequest` to keep dependent sequencing along with auto-batching.\nThis is done with the `Parallel` instance for `LazyRequest` so you can call `.parTupled`/`.parTraverse` and so on on your independent fetches, and get automatic deduplication and batching just like you did in the original Fetch library, only now you must explicitly opt-in to auto-batching.\n\nFor more details, please see the included tests.\n\n# Copyright\n\nfetchless is designed and developed by 47 Degrees Open Source\n\nCopyright (C)  2022 47 Degrees Open Source \u003chttps://www.47deg.com\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F47degrees%2Ffetchless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F47degrees%2Ffetchless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F47degrees%2Ffetchless/lists"}