{"id":17997361,"url":"https://github.com/thma/clean-architecture-with-functions","last_synced_at":"2025-10-13T23:34:21.782Z","repository":{"id":181579339,"uuid":"666991226","full_name":"thma/clean-architecture-with-functions","owner":"thma","description":"Clean Architecture in Haskell. Using higher order functions for 'dependency injection'","archived":false,"fork":false,"pushed_at":"2025-02-01T19:13:24.000Z","size":280,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-04T06:23:13.118Z","etag":null,"topics":["clean-architecture","clean-code","dependency-injection","functional-programming","haskell","inversion-of-control","patterns"],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thma.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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}},"created_at":"2023-07-16T09:19:51.000Z","updated_at":"2023-09-17T17:05:30.000Z","dependencies_parsed_at":"2023-10-11T08:59:40.339Z","dependency_job_id":null,"html_url":"https://github.com/thma/clean-architecture-with-functions","commit_stats":{"total_commits":11,"total_committers":1,"mean_commits":11.0,"dds":0.0,"last_synced_commit":"ed049e1e4ce6a1e31a7f64fdd56c8e9526295a95"},"previous_names":["thma/clean-architecture-with-functions"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/thma/clean-architecture-with-functions","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thma%2Fclean-architecture-with-functions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thma%2Fclean-architecture-with-functions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thma%2Fclean-architecture-with-functions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thma%2Fclean-architecture-with-functions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thma","download_url":"https://codeload.github.com/thma/clean-architecture-with-functions/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thma%2Fclean-architecture-with-functions/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279017234,"owners_count":26086015,"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","status":"online","status_checked_at":"2025-10-13T02:00:06.723Z","response_time":61,"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":["clean-architecture","clean-code","dependency-injection","functional-programming","haskell","inversion-of-control","patterns"],"created_at":"2024-10-29T21:18:08.553Z","updated_at":"2025-10-13T23:34:21.763Z","avatar_url":"https://github.com/thma.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Clean Architecture revisited\n\n## Interesting design challenges in seemingly simple programs\n\nThe other day I was writing a Haskell program with quite a limited scope:\n\n- retrieving data from a REST API\n- storing the data in CSV files.\n\nThe task at hand sounded simple enough to just start coding without too much upfront thinking.\nThis blog post is about the process of discovering the shortcomings of my initial design and how I improved them by some simple refactorings.\nAs all the interesting stuff happened in the API access, I'll focus on that part of the code.\n\nIn order to allow you to experiment with the code yourself, I'm not using the proprietary API of my original project but rather a publicly available REST API (https://openlibrary.org/developers/api) in this blog post.\n\n## The initial design\n\nSo without further ado, let's start with the domain data types:\n\n````haskell\ndata Book = Book\n  { bkTitle   :: String,\n    bkAuthors :: [String],\n    bkYear    :: Maybe Int\n  }\n  deriving (Eq, Show)\n\ninstance FromJSON Book where\n  parseJSON (Object b) =\n    Book\n      \u003c$\u003e b .: \"title\"\n      \u003c*\u003e b .:? \"author_name\" .!= []\n      \u003c*\u003e b .:? \"first_publish_year\"\n  parseJSON _ = mempty\n````\n\nThe `Book` type is a simple record type with a few fields. The `FromJSON` instance is used to parse the JSON data returned by the REST API.\n\nNext, we need a function to retrieve the data from the REST API. The API allows us to retrieve the data in pages. Each page contains a list of books and the total number of books found. The following function retrieves a single page. The parameters are the query string, the page size and the page number:\n\n````haskell\ngetBookPage :: String -\u003e Int -\u003e Int -\u003e IO BookResp\ngetBookPage queryString pageSize pageId = do\n  request \u003c- parseRequest $ \n    searchUrl ++ queryString ++ \n      \"\u0026page=\" ++ show pageId ++ \n      \"\u0026limit=\" ++ show pageSize\n  response \u003c- httpJSON request\n  return $ getResponseBody response\n\ndata BookResp = BookResp\n  { brDocs  :: [Book],\n    brFound :: Int\n  }\n  deriving (Eq, Show)\n\ninstance FromJSON BookResp where\n  parseJSON (Object br) =\n    BookResp\n      \u003c$\u003e br .: \"docs\"\n      \u003c*\u003e br .: \"numFound\"\n  parseJSON _ = mempty\n\nsearchUrl :: String\nsearchUrl = \"http://openlibrary.org/search.json?q=\"\n````\n\nBased on this function which retrieves a single page, we can write a function which retrieves all pages:\n\n````haskell \nsearchBooks :: Int -\u003e Int -\u003e String -\u003e IO [Book]\nsearchBooks pageSize limitPages queryString = do\n  firstPage \u003c- getBookPage queryString pageSize 1 -- index starts at 1 !\n  let numOfBooks = brFound firstPage\n      numPages = min (numOfBooks `div` pageSize + 1) limitPages\n      otherPages = \n        if numPages == 1 \n          then [] \n          else map (getBookPage queryString pageSize) [1 .. numPages]\n  allPages \u003c- (firstPage :) \u003c$\u003e sequence otherPages\n  return $ concatMap brDocs allPages\n````  \n\nThe `searchBooks` function takes a page size, a limit for the number of pages and a query string. \nIt then retrieves all pages and concatenates the books from all pages into a single list.\n\nNow we have all the pieces in place to write the main function:\n\n````haskell\nmain :: IO ()\nmain = do\n  -- search for books with \"Haskell Curry\" in title or author fields, \n  -- limit to 10 pages of 50 books each (== 500 books)\n  books \u003c- searchBooks 50 10 \"Haskell Language\"\n  putStrLn $ \"Number of matching books: \" ++ show (length books)\n  mapM_ (putStrLn . bkTitle) books\n````\n\nThis will print the number of books found and their respective titles. \n````\nNumber of matching books: 34\n````\n\nA quick manual check on the Open Library website confirms that the number of books found is indeed correct.\n\nThat was is easy, wasn't it? But wait, let's have a closer look by changing the page size...\n\n## It's just an off-by-one error...\n\n````haskell\nmain :: IO ()\nmain = do\n  -- search for books with \"Haskell Curry\" in title or author fields, \n  -- limit to 10 pages of 10 books each (== 100 books)\n  books \u003c- searchBooks 10 10 \"Haskell Language\"\n  putStrLn $ \"Number of matching books: \" ++ show (length books)\n  mapM_ (putStrLn . bkTitle) books\n````\n\nNow we get the following output:\n````\nNumber of matching books: 44\n````\n\nWhat happened here? Instead of the correct number of 34 books we now get 44 books returned.\nIt seems that this was caused by the changing the page size. As we get exactly 10 books more as expected it seems that we get one page more than we asked for.\n\nSo let's have a closer look at the `searchBooks` function.\nIt starts by retrieving the first page which also contains the total number of found books.\nThis value is bound to the `numOfBooks` variable.\nNext the total number of pages is calculated by dividing the number of books by the page size and adding 1.\nBy binding the minimum of this value and the limit for the number of pages to the `numPages` variable, we ensure that we don't retrieve more pages than we asked for.\nFinally, the `otherPages` variable is bound to a list of all pages except the first page.\n\nAnd here we have our off-by-one error. The list comprehension should start at 2 instead of 1:\n\n````haskell \nsearchBooks :: Int -\u003e Int -\u003e String -\u003e IO [Book]\nsearchBooks pageSize limitPages queryString = do\n  firstPage \u003c- getBookPage queryString pageSize 1 -- index starts at 1 !\n  let numOfBooks = brFound firstPage\n      numPages = min (numOfBooks `div` pageSize + 1) limitPages\n      otherPages = \n        if numPages == 1 \n          then [] \n          else map (getBookPage queryString pageSize) [2 .. numPages]\n  allPages \u003c- (firstPage :) \u003c$\u003e sequence otherPages\n  return $ concatMap brDocs allPages\n````  \n\nThis finding seems to be a perfect fit for the following quote:\n\n\u003e There are only two hard problems in computer science: \n\u003e\n\u003e 0. cache invalidation \n\u003e 1. naming things. \n\u003e 2. off-by-one errors\n\n## Do as I say, not as I do.\n\nAt this point I felt a bit uncomfortable as I realized that I should have written some unit tests to catch this error. Or even better to start with the tests and then write the code.\n\nIf you have read some of my other blog posts, you might have noticed that I'm a big fan of unit testing and property based testing in particular. \nSo why didn't I write any tests for this code? \n\nThe answer is simple: `searchBooks` is directly coupled to the page access function `getBookPage`.\nAll unit tests for `searchBooks` would interact directly with the real openlibrary API. This would render the tests unstable as the API could be offline or give different results over time.\n\nSo what can we do about this? We need to decouple the `searchBooks` function from the `getBookPage` function. Functional programming offers us a simple solution for this problem: *higher order functions*. In this case: allowing to pass the page access function as a parameter to `searchBooks`.\n\n## From cruft to craft\n\nIn this section we will refactor the code to allow passing the page access function as a parameter to `searchBooks`.\nLet's start by defining a type for the page access function. I have also changed the `Int` parameters to `Natural` as I don't want to allow negative page sizes and offsets.\n\n````haskell\ntype PageAccess = String -\u003e Natural -\u003e Natural -\u003e IO BookResp\n````\n\nNext we will change the signature of `getBookPage` to match this type:\n\n````haskell\ngetBookPage :: PageAccess\ngetBookPage queryString pageSize pageId = do\n  request \u003c-\n    parseRequest $\n      searchUrl\n        ++ queryString\n        ++ \"\u0026page=\"  ++ show pageId\n        ++ \"\u0026limit=\" ++ show pageSize\n  response \u003c- httpJSON request\n  return $ getResponseBody response\n````\n\nFinally we will rewrite `searchBooks` to have an additional parameter of type `PageAccess`:\n\n````haskell\nsearchBooks :: PageAccess -\u003e Natural -\u003e Natural -\u003e String -\u003e IO [Book]\nsearchBooks bookPageFun pageSize limitPages queryString = do\n  firstPage \u003c- bookPageFun queryString pageSize 1\n  let numOfBooks = brFound firstPage\n      numPages = min (numOfBooks `div` pageSize + 1) limitPages\n      otherPages =\n        if numPages == 1\n          then []\n          else map (bookPageFun queryString pageSize) [2 .. numPages]\n  allPages \u003c- (firstPage :) \u003c$\u003e sequence otherPages\n  return $ concatMap brDocs allPages\n````\n\nThis simple change allows us to use `searchBooks` with different page access functions.\nThe main function now looks like this:\n\n````haskell\nmain :: IO ()\nmain = do\n  -- search for books with \"Haskell Curry\" in title or author fields, \n  -- limit to 10 pages of 10 books each (== 100 books)\n  let openLibrarySearch = searchBooks getBookPage 10 10\n  books \u003c- openLibrarySearch \"Haskell Curry\" \n  putStrLn $ \"Number of matching books: \" ++ show (length books)\n  mapM_ (putStrLn . bkTitle) books\n````\n\n## Finally, some unit tests\n\nWe can now use this decoupling to pass a mock page access function to `searchBooks` in order to write unit tests for it.\nLet's start by writing a mock page access which returns a specified number of books:\n\n````haskell\n-- | A mock implementation of the book page access function.\n--   The argument resultCount specifies the total number of books to return.\nmockBookPageImpl :: Natural -\u003e PageAccess\nmockBookPageImpl _ _ _ 0 = error \"pageId must be \u003e= 1\"\nmockBookPageImpl resultCount _queryString pageSize pageId =\n  let (numFullPages, remainder) = resultCount `quotRem` pageSize\n      numPages = if remainder == 0 then numFullPages else numFullPages + 1\n  in  if pageId \u003c= numFullPages\n        then return $ BookResp (replicate (fromIntegral pageSize) sampleBook) resultCount\n        else if remainder /= 0 \u0026\u0026 pageId == numPages\n          then return $ BookResp (replicate (fromIntegral remainder) sampleBook) resultCount\n          else return $ BookResp [] resultCount\n\nsampleBook :: Book\nsampleBook = Book\n  { bkTitle = \"The Lord of the Rings\",\n    bkAuthors = [\"J. R. R. Tolkien\"],\n    bkYear = Just 1954\n  }\n````\n\nNow we can start to write unit tests for `searchBooks`:\n\n````haskell\nspec :: Spec\nspec = do\n  describe \"Using a paging backend API as data input\" $ do\n    it \"works for empty result\" $ do\n      let mockSearch = searchBooks (mockBookPageImpl 0) 50 10\n      result \u003c- mockSearch \"Harry Potter\"\n      length result `shouldBe` 0\n    it \"respects the max number of pages parameter\" $ do\n      let mockSearch = searchBooks (mockBookPageImpl 100) 5 10\n      result \u003c- mockSearch \"Harry Potter\"\n      length result `shouldBe` 50\n    it \"works correctly in the last page\" $ do\n      let mockSearch = searchBooks (mockBookPageImpl 49) 5 10\n      result \u003c- mockSearch \"Harry Potter\"\n      length result `shouldBe` 49\n    it \"can deal with arbitrary result sizes\" $\n      property $ \\resultSize -\u003e do\n        let mockSearch = searchBooks (mockBookPageImpl resultSize) 5 10\n        result \u003c- mockSearch \"Harry Potter\"\n        length result `shouldBe` fromIntegral (min resultSize 50)\n    it \"can deal with arbitrary page sizes\" $\n      property $ \\ps -\u003e do\n        let pageSize = ps + 1\n            mockSearch = searchBooks (mockBookPageImpl 100) pageSize 10\n        result \u003c- mockSearch \"Harry Potter\"\n        length result `shouldBe` fromIntegral (min 100 (pageSize * 10))\n````\n\nwhich results in the following output:\n\n````sh\nSearchBooks\n  Using a paging backend API as data input\n    works for empty result [✔]\n    respects the max number of pages parameter [✔]\n    works correctly in the last page [✔]\n    can deal with arbitrary result sizes [✔]\n      +++ OK, passed 100 tests.\n    can deal with arbitrary page sizes [✔]\n      +++ OK, passed 100 tests.\n````\n\nSo now we have a unit test suite for `searchBooks` which doesn't depend on the real API. This was made possible by decoupling the page access function from `searchBooks`. \n\n## Decoupling and Clean Architecture\n\nIn order to illustrate the decoupling achieved by this refactoring, I have created a dependency graph of the modules involved. The arrows indicate the direction of the dependencies. The `searchBooks` function (in the `SearchUseCase` module) does only depend on the `PageAccess` type (in the `ApiModel` module) and of course on the `Book` type in the `DomainModel` module. \n\nThe `Main` module takes the `getBookPage` function (from module `ApiAccess`) and passes it to the `searchBooks` function in order to work against the real API.\n`getBookPage` only depends on the `PageAccess` type.\n\n![dependencies](img/dependencies-with-functions.png)\n\n(Generated with [GraphMod](https://hackage.haskell.org/package/graphmod))\n\nThis is exactly the kind of decoupling that the [clean architecture](https://thma.github.io/posts/2020-05-29-polysemy-clean-architecture.html) advocates:\n\n\u003e The overriding rule that makes this architecture work is The Dependency Rule. \n\u003e This rule says that source code dependencies can only point inwards. \n\u003e Nothing in an inner circle can know anything at all about something in an outer circle. \n\u003e In particular, the name of something declared in an outer circle must not be mentioned by the code in the an inner circle. \n\u003e That includes, functions, classes. variables, or any other named software entity.\n\u003e \n\u003e Quoted from [Clean Architecture blog post](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)\n\n![clean architecture](img/clean-architecture-with-functions.png)\n\nI hope that this post has shown you that the clean architecture is not only applicable to large projects. Even in very small projects it can help to decouple business logic from infrastructure code and thus greatly improve testability.\n\nLuckily for us Haskell programmers, we don't need to use any frameworks to achieve this decoupling. We can use higher order functions to achieve the same result.\n\n\u003c!--\normolu -i src/*            \nstylish-haskell -r -i src/*\n--\u003e","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthma%2Fclean-architecture-with-functions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthma%2Fclean-architecture-with-functions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthma%2Fclean-architecture-with-functions/lists"}