Haskell Maintenance

Upgrading to

LTS 16 (GHC 8.8.4) and Beyond

Outline

Part I

  • TVision Haskell Universe
    • Internal / External Libs
    • Wider Haskell Ecosystem
    • Haskell the TVision Way™️
  • Maintaining a Large Haskell Project
    • Maintenance Considerations
    • General Haskell Tradeoffs
    • Haskell the TVision Way™️ Tradeoffs
  • TVision Haskell Upgrade Process
  • Experience Report: LTS 16 Upgrade

Part II

  • LTS 16 Haskell Changes
  • GHC Features
    • Existing Features
    • New Features
    • Upcoming Features

Part III

  • State of...
    • TVision Haskell Dependencies
    • Wider Haskell Ecosystem
    • TVision Backend Codebase

Part I - Maintaining Large Haskell Projects

TVision Haskell Universe

Haskell @ TVision

  • Haskell is the language of our backend:
    • Services: ingest-tracker, upload-manager, device-config
      • ​​Tracker — A workflow engine and the central nervous system of data pipelines until they arrive in Redshift* (where process-manager takes over)
        • Can orchestrate batch processes or track remote ones
        • Invalidate and restate data against newer algorithms
    • Periodically Scheduled Scripts
      • Often batch ingestions from APIs, S3, (S)FTP, etc
    • And kitchen sink..

*Tracker also manages GoodData loads

Haskell Repos at TVision

  • sauron
    • Monorepo: services, periodically scheduled scripts, configuration files, postgres migrations, dev tools, and shared Haskell utilities
  • mason
    • Build scripts for monorepo
  • circle-docker-haskell
    • Pinned package set for all our code
    • Docker image for building, testing, and deploying haskell artifacts in CI

Developing Sauron

  • Magnitude
    • ~290KLOC in sauron
      • ~75KLOC of Haskell (~1/3 of python codebase)
      • KLOC is misleading, esp. in Haskell
        • Outside of type spellings, Haskell is dense for a general purpose language 
  • Large number of tests (but not as much as we'd like)
  • Contributions + Upkeep
    • Built from 9 major contributors over 4 ½ years
    • Currently 2 full-ish time devs doing maintenance and new features
  • Deployed to wholesale to one QA and PROD system  each

Internal Dependency Graph

// Generate internal dep. graph visual
$ stack dot | dot -Tpng -o sauron.png
// Number of internal packages
$ stack ls dependencies --no-external | wc -l
      65

Internal Haskell Packages

External Haskell Packages

External Dependency Graph

  • Includes internal and transitive deps
//Generate internal + external dep. graph visual
$ stack dot --no-include-base --external --depth=1 \
  | dot -Tpng -o sauron-external.png
// Number of internal + external packages
$ stack ls dependencies --external | wc -l
     438

Magnitude of Sauron

// Lines of Haskell code
sauron hkailahi$ cloc --not-match-d='.*stack.*' --fullpath --include-ext=hs .
    3511 text files.
    3323 unique files.
    3038 files ignored.

github.com/AlDanial/cloc v 1.90  T=1.77 s (466.0 files/s, 54333.7 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Haskell                        824          11403           9151          75511
-------------------------------------------------------------------------------
SUM:                           824          11403           9151          75511
-------------------------------------------------------------------------------
// Lines of Haskell test code
sauron hkailahi$ find . -name "*Spec.hs" | xargs cloc
     212 text files.
     212 unique files.
       0 files ignored.

github.com/AlDanial/cloc v 1.90  T=0.12 s (1757.6 files/s, 249591.8 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Haskell                        212           3414           1192          25500
-------------------------------------------------------------------------------
SUM:                           212           3414           1192          25500
-------------------------------------------------------------------------------
// Lines of python, configuration, etc
sauron hkailahi$ cloc --not-match-d='.*stack.*,venv' \
  --fullpath --include-ext=py,json,yaml,yml, .
    4301 text files.
    4009 unique files.
    3350 files ignored.

github.com/AlDanial/cloc v 1.90  T=11.02 s (124.3 files/s, 28290.2 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Python                        1206          44408          55747         179448
JSON                            28              0              0          16462
YAML                           136           1444             90          14193
-------------------------------------------------------------------------------
SUM:                          1370          45852          55837         210103
-------------------------------------------------------------------------------

Haskell Ecosystem

  • Package Distribution
    • Package Registries / Indexes
      • Hackage
      • Stackage
    • From Source
      • Git: ​​Github, Bitbucket, etc
  • Build Tools
    • Cabal
    • Stack
    • Nix

Stack Ecosystem

  • Stackage LTS
    • Stable package sets from Hackage using a specific version of the GHC Haskell compiler
    • Easy to use with stack
  • Hosted and developed by FPComplete
    • Relies on community contributions to submit individual packages and keep them up-to-date

Haskell the TVision Way™️

  • One custom package set for ALL code at a time
    • Defined on top of a Stackage LTS snapshot
  • High Code Sharing
  • Functional core, imperative shell
    • Push effects to the boundaries
  • Unafraid of (most) advanced language features
  • Verbose Code Style
    • variable, function, type names
  • Reduce boilerplate with heavy code generation

Maintaining a Large Haskell Project

Considerations

  • Maintenance (Stability + Feature Parity)
    • Upfront
    • Sustained
  • ​Surface Area / Blast Radius
    • ​Audit
  • ​Developer UX
    • Compile Times
    • Code/Feature Discovery
    • Tools dictate action

Dependency Blast Radius

  • ​Coupling can exist for types, functions, modules, packages, projects, etc
  • Classifying Coupling

Coined by He-Who-Must-Not-Be-Named-Uncle-Bob

Coupling Example

### Foobar.py
------------------------------
class Quux(NamedTuple):
  id: int

class Foo(NamedTuple):
  q: Quux

class Bar(NamedTuple):
  q: Quux

Instability Index

  • Various tools can generate this for function, modules, packages, etc
  • Metric not at all useful on it's own, but has potential use with wider context
    • More sophisticated metrics for stability exist

Code Organization

  • Monorepo
    • Haskell's superpower is refactoring
      • Monorepos compliment to Haskell's strengths, while polyrepos defeat them
  • Custom Prelude: standard library we've chosen
  • Shared Utilities: tvision-shared
    • Common utilities are lifted to the top level (horizontally organized)
  • Vertical* Multi-Package Projects
    • Services - *-core, *-api, *-manager, *-handler,  *-metrics
    • Scripts - *-core, *-scripts, *-database, *-api/*-client

*Some horizontal projects, like tracker, are depended on everywhere

It's a Monorepo!

Henelis-MacBook-Pro-2:sauron hkailahi$ tree -L 1
.
├── README.md
├── acr-database
├── acr-monitor-core
├── acr-monitor-scripts
├── acrcloud-client
├── acrcloud-core
├── ad-import-etl-core
├── ad-import-etl-scripts
├── ad-metadata-api
├── ad-metadata-database
├── api
├── auth-api
├── build-scripts
├── content-metadata-api
├── content-metadata-database
├── device-config-api
├── device-config-core
├── device-config-handler
├── device-config-manager
├── device-config-metrics
├── device-config-scripts
├── firehose-database
├── gooddata-api
├── gooddata-provisioning-api
├── gooddata-provisioning-core
├── gooddata-provisioning-scripts
├── gtv-scripts
├── hie.yaml
├── import-core
├── in-tab-core
├── in-tab-scripts
├── ingest-api
├── ingest-core
├── ingest-driver
├── ingest-integration
├── ingest-manager
├── ingest-metrics
├── ingest-scripts
├── ingest-settings
├── ingest-tracker
├── ingest-tracker-database
├── kinetiq-api
├── kinetiq-core
├── kinetiq-scripts
├── migrate
├── operations-database
├── operations-database-scripts
├── panel-api
├── pentaho-invoker-core
├── pentaho-invoker-scripts
├── photo-training-api
├── photo-training-handler
├── photo-training-metrics
├── python-scripts
├── ranking-unload-api
├── ranking-unload-core
├── ranking-unload-scripts
├── sauron-external.png
├── sauron.png
├── stack.yaml
├── stack.yaml.lock
├── test-results
├── tivo-etl-api
├── tivo-etl-core
├── tivo-etl-scripts
├── tvision-16.31-201.yaml
├── tvision-shared
├── tvision-shared-example
├── tvision-shared-scripts
├── upload-api
├── upload-core
├── upload-handler
├── upload-manager
├── upload-metrics
├── vidvita-client
├── vidvita-core
├── zoho-api
└── zoho-synch

Haskell Maintenance Advantages

  • Refactoring
    • Discovery of breaking changes
      • Compile Time > Run Time
      • Safer Code Evolution
    • Easily create + incorporate wide scale improvements (all the way down)
  • Code Quality

Haskell Maintenance Disadvantages

  • Compile Times
  • Fast moving compiler and ecosystem
  • Typeclass Compatibility
    • Superclasses
      • AMP, Semigroup, MonadFail Proposals
    • Orphan Instances
  • ​Documentation Quality
  • Esoteric
    • Small community
    • Requires active engagement
      • Upstream Maintenance
      • Writing own libraries

Haskell the TVision Way

Maintenance Advantages

  • Mechanical Updates
    • Fearless refactoring
    • Actually refactor and maintain code rather than rewrite every N years
  • Correctness*
  • Upkeep of Code and Tool Sharing
  • Local Dev + Integration Test Capabilities

*No more likely to prevent a logical error

Haskell the TVision Way

Maintenance Disadvantages

TVision Haskell Upgrade Process

New Snapshot Available

  • Stackage publishes a new Stackage LTS
    • Ideally we stay a couple months behind major release

Upgrade Planning

  • Create a ticket for available LTS major upgrade
  • Passively track:
    • Known major changes
    • State of major third-party dependencies
    • New language features
    • Announcements (security, breakages, etc)

Updating Custom Snapshot

Updating Haskell Code

  • Update relevant Haskell repos
    • mason (build scripts)
    • sauron (monorepo)

Deploy Changes

  • Deploy
    • Continuous QA Deployment of ALL sauron
    • Manual PROD deployment of ALL sauron
      • Practice as of now
      • Historically, components have typically been deployed based on need, which means potential changes often sit until LTS deploy
        • This can (and should) be improved

Experience Report: LTS 16 Upgrade

New Snapshot Available + Upgrade Planning

Stackage LTS Releases

  • LTS 11 (ghc-8.2.2) on 2018-03-12
  • LTS 12 (ghc-8.4.3) on 2018-07-09
  • LTS 13 (ghc-8.6.3) on 2018-12-23
  • LTS 14 (ghc-8.6.5) on 2019-08-05
  • LTS 15 (ghc-8.8.2) on 2020-02-16
  • LTS 16 (ghc-8.8.3) on 2020-06-08
  • LTS 17 (ghc-8.10.3) on 2021-01-24
  • Nightlies on ghc-9.0.1 on 2021-05-29

TVision Upgrades

Updating Custom Snapshot

  • Stackage LTS Dependency Omission
  • "Foreign Dependencies"
    • Removed
      • cryptonite, file-path-th, HasBigDecimal

    • Retained
      • interpolator, libssh2

    • New

      • Available on future LTS
        • interpolator, vinyl
  • New "Foreign Dependencies" continued
    • Not available on new LTS
      • Source Repo
        • uri-templater
        • libssh2
      • ​Stale
        • ​postgresql-simple-migration, tz
        • Haddock + Dash: haddocset, haddock-api, haddock-library
      • Stale + License Issues
        • token-bucket (HVR)
        • wai-middleware-throttle (creichert, dfithian)

Updating CI Image

Updating Haskell Builds

  • LTS PRs – mason, sauron
  • Builds
    • CI build times relatively unchanged (~20 minutes)
    • 🔥🔥🔥🔥 stack 🔥🔥🔥🔥
    • ​mason / shake
      • Another possible culprit of recompilation?? 🤔

Updating Haskell Code

  • LTS PRs – mason, sauron
    • Too many files for typical Bitbucket PR view
      • Only shows one file at a time
-- Sauron diff between lts branch & pre-merged master
$ git diff --shortstat \
  478239fe85a97b3df8897516ebe64b6e0a9b0fe9 \
  72ee1b74114c051e8226531c47157077d24a4fe3
 255 files changed, 1406 insertions(+), 1044 deletions(-)

Updating Haskell Code

Notes (Will cover in Part II)

 

# LTS 16 upgrade

## Breaking Changes

* `MonadFail` Proposal
	* Not sure what the monad to throw in? Using `throwIO . userError` as substitute because I’m bad at software
	* Try to avoid `MonadFail.fail` at all costs
		* Use `parseFail` with `Aeson`, which `= fail` but specific to `FromJSON` implementation which is the point of avoiding `fail`
		* `MonadFail` usages (for now)
				* `Q` for TH (compile-time error at least, but still gross)
	* No `Either String` instance
		* https://twitter.com/chris__martin/status/1095839981845790724
		* https://twitter.com/taylorfausak/status/1180158792622903298
		* `Relude` has instance https://github.com/kowainik/relude/blob/78c307f948c52b0b976fe5a588825a1623d9348a/src/Relude/Monad/Either.hs#L71-L73
		* Evidently our datetime parsers were partial
		* 

* `Path`
	* Now need annotation with type in scope now https://github.com/commercialhaskell/path/issues/161#issuecomment-632041470

```
-- |The repository root, as an absolute path.
getRootDir :: Shake.Action FilePath
getRootDir = do
  path <- liftIO $ makeAbsolute @(Path Rel Dir) [reldir|.|]
  pure $ toFilePath path
```

* `Servant`
	* `ServantErr` -> `ServerError`
	* `ServantError` -> `ClientError`
		* `FailureResponse` now includes the failing request, which is nice. I just dropped it where I saw it but  it’s probably useful to log it?

* Swagger
	* `& type_ .~`  to  `& type_ ?~` because optional
	* `InsOrdHashSet` on `_swaggerTags`

* `Esqueleto`
	* No more `Esqueleto` type, concrete > polymorphic
		* `expr` -> `SqlExpr`
		* `Esqueleto _ _ backend` -> `SqlBackend`
	* `sub_select` x4
		* used `subSelectUnsafe` instead of `subSelectMaybe` because I didn’t want to deal with change in type and how to get neighbor combinators working (ex. `Q.not_`)
```
panel-api                    > /Users/hkailahi/tvision/git/haskell/sauron/panel-api/src/Panel/Database/DeviceReadActions.hs:260:47: error: [-Wdeprecations, -Werror=deprecations]
panel-api                    >     In the use of ‘sub_select’
panel-api                    >     (imported from Database.Esqueleto, but defined in Database.Esqueleto.Internal.Internal):
panel-api                    >     Deprecated: "sub_select
panel-api                    >  sub_select is an unsafe function to use. If used with a SqlQuery that
panel-api                    >  returns 0 results, then it may return NULL despite not mentioning Maybe
panel-api                    >  in the return type. If it returns more than 1 result, then it will throw a
panel-api                    >  SQL error.
panel-api                    >
panel-api                    >  Instead, consider using one of the following alternatives:
panel-api                    >  - subSelect: attaches a LIMIT 1 and the Maybe return type, totally safe.
panel-api                    >  - subSelectMaybe: Attaches a LIMIT 1, useful for a query that already
panel-api                    >    has a Maybe in the return type.
panel-api                    >  - subSelectCount: Performs a count of the query - this is always safe.
panel-api                    >  - subSelectUnsafe: Performs no checks or guarantees. Safe to use with
panel-api                    >    countRows and friends."
panel-api                    >     |
panel-api                    > 260 |   in Q.case_ [ (Q.exists $ void latestAction, Q.sub_select latestIsAction) ] (Q.val False)
panel-api                    >     |
```


* `Persistent`
	* Now requires:
```
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DerivingStrategies #-}
```

## Extra
* `ClassyPrelude`
	* Now exports a lot more
		* `time` aka Data.Time.* like `Day`, `isoDefaultLocalTime`, `formatTime`, etc
			* Was `formatTime` getting deprecated????
		* `Control.Monad.Reader`  like `ReaderT`
		* `Control.Monad.IO.Class`  like `MonadIO`, `liftIO`
		* `UnliftIO.Exception` and probably the rest of `unliftio`
			* `import UnliftIO.Temporary (withSystemTempDirectory)`
			* `UnliftIO.Async`
		* `stm` like `TVar`
				* `containers` like `Data.HashMap.Lazy`

## Cleanup
* Annotating or inlining `Fail.fail`
* `error "blah"` -> `impureThrow $ flip StringException callStack “blah”`
	* These are still partial, but now at least our impure exceptions are intentional and HAVE A CALLSTACK 🎉

## Random

We had redundant `MonadReader`, `MonadCatch`, `HasEnv` constraints on foundational AWS stuff, which for some reason we decided to defer with `{-# OPTIONS_GHC -Wno-redundant-constraints #-}`


## Wat 1 - 

## Wat  2 - `device-config-core` missing
```
device-config-handler        > <command line>: cannot satisfy -package-id device-config-core-4.0-Hv6wPjw7xB74Wc27BUkz9N                                                                                         device-config-handler        >     (use -v for more information)
...
...
device-config-scripts        > Configuring device-config-scripts-4.0...
device-config-scripts        > Cabal-simple_mPHDZzAJ_3.0.1.0_ghc-8.8.4: The following package dependencies
device-config-scripts        > were requested
device-config-scripts        > --dependency='device-config-core=device-config-core-4.0-Hv6wPjw7xB74Wc27BUkz9N'
device-config-scripts        > however the given installed package instance does not exist.
```

## Wat 3 - Test build resource blocking errors? 
* `build-lock`

Looks like `HLS` and `mason` are competing (running their own colliding stack processes)


```
Error when running Shake build system:
  at action, called at src/Shared/Build/Rules.hs:171:3 in tvision-shared-build-1.0-2Kuf4TFsxabFHFPjQLm3rE:Shared.Build.Rules
  at need, called at src/Shared/Build/Rules.hs:174:5 in tvision-shared-build-1.0-2Kuf4TFsxabFHFPjQLm3rE:Shared.Build.Rules
* Depends on: .build/ingest-scripts/doc-test
  at need, called at src/Shared/Build/Actions.hs:326:5 in tvision-shared-build-1.0-2Kuf4TFsxabFHFPjQLm3rE:Shared.Build.Actions
* Depends on: .build/ingest-scripts/compile
  at cmd_, called at src/Shared/Build/Actions.hs:274:33 in tvision-shared-build-1.0-2Kuf4TFsxabFHFPjQLm3rE:Shared.Build.Actions
* Raised the exception:
Development.Shake.cmd, system command failed
Command line: stack build ingest-scripts --test --no-run-tests --fast
Exit code: 1
Stderr:
ingest-tracker-database> blocking for directory lock on /Users/hkailahi/tvision/git/backend/sauron/ingest-tracker-database/.stack-work/dist/x86_64-osx/Cabal-3.0.1.0/build-lock
ingest-tracker-database> configure (lib)
ingest-tracker-database> Configuring ingest-tracker-database-4.0...
ingest-tracker-database> build (lib)
ingest-tracker-database> Preprocessing library for ingest-tracker-database-4.0..
ingest-tracker-database> Building library for ingest-tracker-database-4.0..
ingest-tracker-database> [ 1 of 13] Compiling Ingest.Tracker.Database [Optimisation flags changed]
ingest-tracker-database> <command line>: dlopen(/Users/hkailahi/tvision/git/backend/sauron/.stack-work/install/x86_64-osx/bd89f4eaefc06cd173fc3ea35b5bcb82189eb2837d9754919e73596a7e16914b/8.8.4/lib/x86_64-osx-ghc-8.8.4/libHSingest-settings-4.0-LNA4Yc0UglxCy6qnNu5PEK-ghc8.8.4.dylib, 5): Symbol not found: _ingestzmapizm4zi0zmCKj364Un0DC2Co9M8Fvp9R_IngestziApiziDriverTypes_zdfFromJSONSparkLocator2_closure
ingest-tracker-database>   Referenced from: /Users/hkailahi/tvision/git/backend/sauron/.stack-work/install/x86_64-osx/bd89f4eaefc06cd173fc3ea35b5bcb82189eb2837d9754919e73596a7e16914b/8.8.4/lib/x86_64-osx-ghc-8.8.4/libHSingest-settings-4.0-LNA4Yc0UglxCy6qnNu5PEK-ghc8.8.4.dylib
ingest-tracker-database>   Expected in: /Users/hkailahi/tvision/git/backend/sauron/.stack-work/install/x86_64-osx/bd89f4eaefc06cd173fc3ea35b5bcb82189eb2837d9754919e73596a7e16914b/8.8.4/lib/x86_64-osx-ghc-8.8.4/libHSingest-api-4.0-CKj364Un0DC2Co9M8Fvp9R-ghc8.8.4.dylib
ingest-tracker-database>  in /Users/hkailahi/tvision/git/backend/sauron/.stack-work/install/x86_64-osx/bd89f4eaefc06cd173fc3ea35b5bcb82189eb2837d9754919e73596a7e16914b/8.8.4/lib/x86_64-osx-ghc-8.8.4/libHSingest-settings-4.0-LNA4Yc0UglxCy6qnNu5PEK-ghc8.8.4.dylib
Progress 1/6

--  While building package ingest-tracker-database-4.0 (scroll up to its section to see the error) using:
      /Users/hkailahi/.stack/setup-exe-cache/x86_64-osx/Cabal-simple_mPHDZzAJ_3.0.1.0_ghc-8.8.4 --builddir=.stack-work/dist/x86_64-osx/Cabal-3.0.1.0 build lib:ingest-tracker-database --ghc-options ""
    Process exited with code: ExitFailure 1

```


### Wat 4 - IOError != SomeException?
```
kinetiq-core                 > [ 6 of 10] Compiling Kinetiq.Core.ImportSpec                                                                                                                                     kinetiq-core                 >                                                                                                                                                                                  kinetiq-core                 > /Users/hkailahi/tvision/git/backend/sauron/kinetiq-core/test/Kinetiq/Core/ImportSpec.hs:306:15: error:                                                                           kinetiq-core                 >     • Couldn't match type ‘SomeException’ with ‘IOException’                                                                                                                     kinetiq-core                 >       Expected type: IOError -> ConduitT i o m CursorMark                                                                                                                        kinetiq-core                 >         Actual type: SomeException -> ConduitT i o m CursorMark                                                                                                                  kinetiq-core                 >     • The first argument of ($) takes one argument,                                                                                                                              kinetiq-core                 >       its type is ‘cat0 a0 c0’,                                                                                                                                                  kinetiq-core                 >       it is specialized to ‘SomeException -> ConduitT i o m CursorMark’                                                                                                          kinetiq-core                 >       In the expression:                                                                                                                                                         kinetiq-core                 >         map (const CursorMarkAll)                                                                                                                                                kinetiq-core                 >           . yieldM . throwIO . StreamingErrorServant . ConnectionError                                                                                                           kinetiq-core                 >           $ userError "test connection error"                                                                                                                                    kinetiq-core                 >       In an equation for ‘creativeStreamFail’:                                                                                                                                   kinetiq-core                 >           creativeStreamFail                                                                                                                                                     kinetiq-core                 >             = map (const CursorMarkAll)                                                                                                                                          kinetiq-core                 >                 . yieldM . throwIO . StreamingErrorServant . ConnectionError                                                                                                     kinetiq-core                 >                 $ userError "test connection error"                                                                                                                              kinetiq-core                 >     |                                                                                                                                                                            kinetiq-core                 > 306 |               map (const CursorMarkAll)
```

TODO - Re-enable after re-running shake update-swagger-specs

## Wat 5 - Doc Build Misread Correct Module Name and Failed it as incorrect/mismatching the file location
https://app.circleci.com/pipelines/bitbucket/TVision-Insights/sauron/4889/workflows/9eeadb2a-9adc-4707-8721-996f58fcfdf1/jobs/5260/parallel-runs/2?filterBy=FAILED

```
 auth-api                     > 
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     > src/Auth/Api/Database/Model.hs:1:1: error:
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >     File name does not match module name:
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >     Saw: ‘Main’
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >     Expected: ‘Auth.Api.Database.Model’
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >   |
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     > 1 | {-# LANGUAGE UndecidableInstances #-}
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >   | ^
```

But file clearly uses `Auth.Api.Database.Model`

```
{-# LANGUAGE UndecidableInstances #-}
-- ^Required for using `persistent`

module Auth.Api.Database.Model where

...
```

This isn’t an issue in other places? There’s an issue that says this the error when the module is unparseable by haddock, though I’m not sure what’s in the way https://github.com/commercialhaskell/stack/issues/1549

Probably the comment underneath it not being `module`?

Deploy Changes

  • Deploy MAIN
    • Monitored QA deploy for a couple days, then proceeded PROD deploy
      • Everything worked! 🎉
    • Added deploy steps + example to custom snapshot docs
  • Time Taken (including typical distractions)
    • Ticket Start to PR Approval — 3 developer weeks (15 days)
    • PR Approval to Deploy — ~1.5 developer weeks (10 days)
      • ​Lots of on-call FIREs
    • Larger than typical LTS upgrade
      • Also extended by choosing to do lots of tech debt refactoring

Questions?

  • Anything you want to hear more about next week?
    • Or look at now?
  • Any maintenance war stories?

Part II - GHC Upgrades + Features

Including LTS 16 (GHC 8.8) Upgrade Notes

Contents

  • Haskell 101
  • LTS 16 Upgrade (Haskell focus)
  • GHC Features

Haskell 101

A 5 Minute Introduction

Left: https://serokell.medium.com/haskell-history-of-a-community-powered-language-b720ff6b54d

Right: https://www.futurelearn.com/info/courses/functional-programming-haskell/0/steps/27218

Haskell the language is/has...

  • "Purely Functional"
    • Immutable data
    • Lazy evaluation
    • Managed Effects (-ish)
  • "Strong Static Typing"
    • Algebraic Data Types
    • Parametric and Ad Hoc Polymorphism
      • Overloading via Typeclasses
      • Many type-level features
  • Useful (in industry) to be aware of...
    • Optimizing Compiler
    • (A)synchronous Exceptions

The Haskell Standard Library is/has...

  • Expressive
    • Language-level features in other languages are simple functions
      • Ex. Async/Await, Loops, Short-Circuiting, Accumulative Validation, Managed Effects, Parallelism, Etc
  • Performant Runtime
    • Concurrency
      • Green threads
      • Software Transactional Memory
    • Compiled
      • Rewriting, Specializing/Inlining, Levity Polymorphism, etc
  • Type Safety
    • GADTs, Type Families, GHC.Generics, Typeclass Coherence, etc

Haskell Primer

Haskell 401

(Not Introductory)

So anyways, here's my monad tutorial....

Relevant to MonadFail and QualifiedDo discussions

Monads 101

  • Monads give Haskell programmers an interface for combining computations under some shared context

    • This context is given as a type, and it's behavior is specified by that type's monad implementation

instance Monad Maybe ...
instance Monad (Either e) ...
instance Monad IO ...
instance Monad Reader ...
instance Monad State ...

Without Monads

  • Not necessary* to use monadic interface

    • Language provides a familiar, imperative looking syntax sugar that is nice to use and understand.

# Without Syntax
aShortCircuitingFn :: Either SomeError SomeResult
aShortCircuitingFn =
  case doThingOrFail of 
    Left err1 -> Left err1
    Right result1 -> 
      case doThingWithResult1OrFail result1 of
        Left err2 -> Left err2
        Right result2 -> Right result2

* IO doesn't export constructor, but does have Monad instance

Sugar OOooOOoo

  • Do-syntax

    • a familiar, imperative looking syntax sugar for Monads

# Monad Do Syntax
aShortCircuitingFn :: Either SomeError SomeResult
aShortCircuitingFn = Either.do
  result1 <- doThingOrFail
  result2 <- doThingWithResult1OrFail result1
  pure result2

Do-Syntax

someShortCircuitingFn :: Either SomeError SomeResult
someShortCircuitingFn = Either.do
  result1 <- doThingOrFail
  result2 <- doThingWithResult1OrFail result1
  pure result2

someAsyncComputation :: Async ()
someAsyncComputation = Async.do
  response1 <- makeAsyncRequest1
  response2 <- makeAsyncRequest2
  doSomething response1
  doSomething response2
  
printThings :: IO ()
printThings = IO.do
  input <- getLine
  print input
  
  

Common interface for asynchronous, short-circuiting, stateful, non-deterministic, effectful, linear, and/or other computations

Monad Heirarchy

  • Functor Applicative Monad hierarchy

    • Every Monad is an Applicative and every Applicative is a Functor.  

    • Applicatives are weaker than monads, and can be used to sequence independent computations

      • Can't chain dependent ones as with Monads

    • Functors one the other hand can apply one or several transformations to one computation

      •  Can’t sequence multiple computations

class Functor f where ...
class (Functor f) => Applicative f where ...
class (Applicative f) => Monad f where ...

LTS 16 Upgrade

Haskell-Specific Code Changes

Updating Haskell Code

Notes (Will cover in Part II)

 

# LTS 16 upgrade

## Breaking Changes

* `MonadFail` Proposal
	* Not sure what the monad to throw in? Using `throwIO . userError` as substitute because I’m bad at software
	* Try to avoid `MonadFail.fail` at all costs
		* Use `parseFail` with `Aeson`, which `= fail` but specific to `FromJSON` implementation which is the point of avoiding `fail`
		* `MonadFail` usages (for now)
				* `Q` for TH (compile-time error at least, but still gross)
	* No `Either String` instance
		* https://twitter.com/chris__martin/status/1095839981845790724
		* https://twitter.com/taylorfausak/status/1180158792622903298
		* `Relude` has instance https://github.com/kowainik/relude/blob/78c307f948c52b0b976fe5a588825a1623d9348a/src/Relude/Monad/Either.hs#L71-L73
		* Evidently our datetime parsers were partial
		* 

* `Path`
	* Now need annotation with type in scope now https://github.com/commercialhaskell/path/issues/161#issuecomment-632041470

```
-- |The repository root, as an absolute path.
getRootDir :: Shake.Action FilePath
getRootDir = do
  path <- liftIO $ makeAbsolute @(Path Rel Dir) [reldir|.|]
  pure $ toFilePath path
```

* `Servant`
	* `ServantErr` -> `ServerError`
	* `ServantError` -> `ClientError`
		* `FailureResponse` now includes the failing request, which is nice. I just dropped it where I saw it but  it’s probably useful to log it?

* Swagger
	* `& type_ .~`  to  `& type_ ?~` because optional
	* `InsOrdHashSet` on `_swaggerTags`

* `Esqueleto`
	* No more `Esqueleto` type, concrete > polymorphic
		* `expr` -> `SqlExpr`
		* `Esqueleto _ _ backend` -> `SqlBackend`
	* `sub_select` x4
		* used `subSelectUnsafe` instead of `subSelectMaybe` because I didn’t want to deal with change in type and how to get neighbor combinators working (ex. `Q.not_`)
```
panel-api                    > /Users/hkailahi/tvision/git/haskell/sauron/panel-api/src/Panel/Database/DeviceReadActions.hs:260:47: error: [-Wdeprecations, -Werror=deprecations]
panel-api                    >     In the use of ‘sub_select’
panel-api                    >     (imported from Database.Esqueleto, but defined in Database.Esqueleto.Internal.Internal):
panel-api                    >     Deprecated: "sub_select
panel-api                    >  sub_select is an unsafe function to use. If used with a SqlQuery that
panel-api                    >  returns 0 results, then it may return NULL despite not mentioning Maybe
panel-api                    >  in the return type. If it returns more than 1 result, then it will throw a
panel-api                    >  SQL error.
panel-api                    >
panel-api                    >  Instead, consider using one of the following alternatives:
panel-api                    >  - subSelect: attaches a LIMIT 1 and the Maybe return type, totally safe.
panel-api                    >  - subSelectMaybe: Attaches a LIMIT 1, useful for a query that already
panel-api                    >    has a Maybe in the return type.
panel-api                    >  - subSelectCount: Performs a count of the query - this is always safe.
panel-api                    >  - subSelectUnsafe: Performs no checks or guarantees. Safe to use with
panel-api                    >    countRows and friends."
panel-api                    >     |
panel-api                    > 260 |   in Q.case_ [ (Q.exists $ void latestAction, Q.sub_select latestIsAction) ] (Q.val False)
panel-api                    >     |
```


* `Persistent`
	* Now requires:
```
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DerivingStrategies #-}
```

## Extra
* `ClassyPrelude`
	* Now exports a lot more
		* `time` aka Data.Time.* like `Day`, `isoDefaultLocalTime`, `formatTime`, etc
			* Was `formatTime` getting deprecated????
		* `Control.Monad.Reader`  like `ReaderT`
		* `Control.Monad.IO.Class`  like `MonadIO`, `liftIO`
		* `UnliftIO.Exception` and probably the rest of `unliftio`
			* `import UnliftIO.Temporary (withSystemTempDirectory)`
			* `UnliftIO.Async`
		* `stm` like `TVar`
				* `containers` like `Data.HashMap.Lazy`

## Cleanup
* Annotating or inlining `Fail.fail`
* `error "blah"` -> `impureThrow $ flip StringException callStack “blah”`
	* These are still partial, but now at least our impure exceptions are intentional and HAVE A CALLSTACK 🎉

## Random

We had redundant `MonadReader`, `MonadCatch`, `HasEnv` constraints on foundational AWS stuff, which for some reason we decided to defer with `{-# OPTIONS_GHC -Wno-redundant-constraints #-}`


## Wat 1 - 

## Wat  2 - `device-config-core` missing
```
device-config-handler        > <command line>: cannot satisfy -package-id device-config-core-4.0-Hv6wPjw7xB74Wc27BUkz9N                                                                                         device-config-handler        >     (use -v for more information)
...
...
device-config-scripts        > Configuring device-config-scripts-4.0...
device-config-scripts        > Cabal-simple_mPHDZzAJ_3.0.1.0_ghc-8.8.4: The following package dependencies
device-config-scripts        > were requested
device-config-scripts        > --dependency='device-config-core=device-config-core-4.0-Hv6wPjw7xB74Wc27BUkz9N'
device-config-scripts        > however the given installed package instance does not exist.
```

## Wat 3 - Test build resource blocking errors? 
* `build-lock`

Looks like `HLS` and `mason` are competing (running their own colliding stack processes)


```
Error when running Shake build system:
  at action, called at src/Shared/Build/Rules.hs:171:3 in tvision-shared-build-1.0-2Kuf4TFsxabFHFPjQLm3rE:Shared.Build.Rules
  at need, called at src/Shared/Build/Rules.hs:174:5 in tvision-shared-build-1.0-2Kuf4TFsxabFHFPjQLm3rE:Shared.Build.Rules
* Depends on: .build/ingest-scripts/doc-test
  at need, called at src/Shared/Build/Actions.hs:326:5 in tvision-shared-build-1.0-2Kuf4TFsxabFHFPjQLm3rE:Shared.Build.Actions
* Depends on: .build/ingest-scripts/compile
  at cmd_, called at src/Shared/Build/Actions.hs:274:33 in tvision-shared-build-1.0-2Kuf4TFsxabFHFPjQLm3rE:Shared.Build.Actions
* Raised the exception:
Development.Shake.cmd, system command failed
Command line: stack build ingest-scripts --test --no-run-tests --fast
Exit code: 1
Stderr:
ingest-tracker-database> blocking for directory lock on /Users/hkailahi/tvision/git/backend/sauron/ingest-tracker-database/.stack-work/dist/x86_64-osx/Cabal-3.0.1.0/build-lock
ingest-tracker-database> configure (lib)
ingest-tracker-database> Configuring ingest-tracker-database-4.0...
ingest-tracker-database> build (lib)
ingest-tracker-database> Preprocessing library for ingest-tracker-database-4.0..
ingest-tracker-database> Building library for ingest-tracker-database-4.0..
ingest-tracker-database> [ 1 of 13] Compiling Ingest.Tracker.Database [Optimisation flags changed]
ingest-tracker-database> <command line>: dlopen(/Users/hkailahi/tvision/git/backend/sauron/.stack-work/install/x86_64-osx/bd89f4eaefc06cd173fc3ea35b5bcb82189eb2837d9754919e73596a7e16914b/8.8.4/lib/x86_64-osx-ghc-8.8.4/libHSingest-settings-4.0-LNA4Yc0UglxCy6qnNu5PEK-ghc8.8.4.dylib, 5): Symbol not found: _ingestzmapizm4zi0zmCKj364Un0DC2Co9M8Fvp9R_IngestziApiziDriverTypes_zdfFromJSONSparkLocator2_closure
ingest-tracker-database>   Referenced from: /Users/hkailahi/tvision/git/backend/sauron/.stack-work/install/x86_64-osx/bd89f4eaefc06cd173fc3ea35b5bcb82189eb2837d9754919e73596a7e16914b/8.8.4/lib/x86_64-osx-ghc-8.8.4/libHSingest-settings-4.0-LNA4Yc0UglxCy6qnNu5PEK-ghc8.8.4.dylib
ingest-tracker-database>   Expected in: /Users/hkailahi/tvision/git/backend/sauron/.stack-work/install/x86_64-osx/bd89f4eaefc06cd173fc3ea35b5bcb82189eb2837d9754919e73596a7e16914b/8.8.4/lib/x86_64-osx-ghc-8.8.4/libHSingest-api-4.0-CKj364Un0DC2Co9M8Fvp9R-ghc8.8.4.dylib
ingest-tracker-database>  in /Users/hkailahi/tvision/git/backend/sauron/.stack-work/install/x86_64-osx/bd89f4eaefc06cd173fc3ea35b5bcb82189eb2837d9754919e73596a7e16914b/8.8.4/lib/x86_64-osx-ghc-8.8.4/libHSingest-settings-4.0-LNA4Yc0UglxCy6qnNu5PEK-ghc8.8.4.dylib
Progress 1/6

--  While building package ingest-tracker-database-4.0 (scroll up to its section to see the error) using:
      /Users/hkailahi/.stack/setup-exe-cache/x86_64-osx/Cabal-simple_mPHDZzAJ_3.0.1.0_ghc-8.8.4 --builddir=.stack-work/dist/x86_64-osx/Cabal-3.0.1.0 build lib:ingest-tracker-database --ghc-options ""
    Process exited with code: ExitFailure 1

```


### Wat 4 - IOError != SomeException?
```
kinetiq-core                 > [ 6 of 10] Compiling Kinetiq.Core.ImportSpec                                                                                                                                     kinetiq-core                 >                                                                                                                                                                                  kinetiq-core                 > /Users/hkailahi/tvision/git/backend/sauron/kinetiq-core/test/Kinetiq/Core/ImportSpec.hs:306:15: error:                                                                           kinetiq-core                 >     • Couldn't match type ‘SomeException’ with ‘IOException’                                                                                                                     kinetiq-core                 >       Expected type: IOError -> ConduitT i o m CursorMark                                                                                                                        kinetiq-core                 >         Actual type: SomeException -> ConduitT i o m CursorMark                                                                                                                  kinetiq-core                 >     • The first argument of ($) takes one argument,                                                                                                                              kinetiq-core                 >       its type is ‘cat0 a0 c0’,                                                                                                                                                  kinetiq-core                 >       it is specialized to ‘SomeException -> ConduitT i o m CursorMark’                                                                                                          kinetiq-core                 >       In the expression:                                                                                                                                                         kinetiq-core                 >         map (const CursorMarkAll)                                                                                                                                                kinetiq-core                 >           . yieldM . throwIO . StreamingErrorServant . ConnectionError                                                                                                           kinetiq-core                 >           $ userError "test connection error"                                                                                                                                    kinetiq-core                 >       In an equation for ‘creativeStreamFail’:                                                                                                                                   kinetiq-core                 >           creativeStreamFail                                                                                                                                                     kinetiq-core                 >             = map (const CursorMarkAll)                                                                                                                                          kinetiq-core                 >                 . yieldM . throwIO . StreamingErrorServant . ConnectionError                                                                                                     kinetiq-core                 >                 $ userError "test connection error"                                                                                                                              kinetiq-core                 >     |                                                                                                                                                                            kinetiq-core                 > 306 |               map (const CursorMarkAll)
```

TODO - Re-enable after re-running shake update-swagger-specs

## Wat 5 - Doc Build Misread Correct Module Name and Failed it as incorrect/mismatching the file location
https://app.circleci.com/pipelines/bitbucket/TVision-Insights/sauron/4889/workflows/9eeadb2a-9adc-4707-8721-996f58fcfdf1/jobs/5260/parallel-runs/2?filterBy=FAILED

```
 auth-api                     > 
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     > src/Auth/Api/Database/Model.hs:1:1: error:
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >     File name does not match module name:
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >     Saw: ‘Main’
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >     Expected: ‘Auth.Api.Database.Model’
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >   |
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     > 1 | {-# LANGUAGE UndecidableInstances #-}
Progress 11/65: auth-api, kinetiq-api, panel-api, vidvita-client                                                                auth-api                     >   | ^
```

But file clearly uses `Auth.Api.Database.Model`

```
{-# LANGUAGE UndecidableInstances #-}
-- ^Required for using `persistent`

module Auth.Api.Database.Model where

...
```

This isn’t an issue in other places? There’s an issue that says this the error when the module is unparseable by haddock, though I’m not sure what’s in the way https://github.com/commercialhaskell/stack/issues/1549

Probably the comment underneath it not being `module`?

Breaking Changes

  • Highlights
    • MonadFail Proposal
    • path Annotations
    • servant-* Errors
    • Swagger Types
    • esqueleto + persistent monomorphized
    • Added classy-prelude re-exports
  • Non-LTS 16 Updates
    • error -> impureThrow

MonadFail Proposal

MonadFail Proposal

MonadFail still an anti-pattern

  • Obscures IOExceptions from pure values
  • We don't need a generic/unqualified way to fail
    • Prefer fail-ing explicitly
-- Default instances
instance MonadFail Maybe where
    fail _ = Nothing
 
instance MonadFail [] where
    fail _ = []
 
instance MonadFail IO where
    fail = failIO -- basically `throwString`

MonadFail Migration

  • Catching all the places we fail-ed and deciding if we really wanted to throw a nameless exception or...
    • Add custom exception
    • Inline existing MonadFail instance
    • Accept upstream MonadFail behavior of inherited type
      • Explicitly annotate, ex. fail @IO "Some codegen error"
      • Use alternative, ex. Aeson provides parseFail = fail
    • Tolerate partiality with FIXME (optparse-applicative cli parsers)
    • Succeed (not fail)
$ count_prev_occs 'fail [\.|\$|\"]'
     237

Servant Improvements

  • Server (servant-server)

    • ServantErr -> ServerError

  • Client (servant-client)

    • ServantError -> ClientError

    • ClientError.FailureResponse now includes the failing request, which is nice

      • I just dropped the field where it came up but it’s would be useful to log it

$ count_prev_occs 'ServantErr'
      98
$ count_prev_occs 'FailureResponse'
      19      

Esqueleto/Persistent Improvements

$ count_prev_occs "[-|=]> \(expr\|query\)"
      53
  • persistent
    • Now requires -XUndecidableInstances +  -XDerivingStrategies
  • esqueleto
    • Monomorphized API
      • From query backend expr (expr (Q.Entity SomeTable)) => expr (q f) SqlExpr (q f)
      • From query backend expr (expr (Q.Entity SomeTable)) => query f SqlQuery f
      • Esqueleto query backend expr => backend SqlBackend
    • Type Safety
      • sub_select subSelectCountRows, subSelectUnsafe, subselectMaybe

Swagger

  • SwaggerType's are now optional
  • Swagger tags are now insert order, instead of alphabetical order
  • Factored out function for differing committed/previously generated vs newly generated swagger docs
  • Didn't check for changes in swagger-generated clients

ClassyPrelude

  • Now re-exports
    • time

      • ​Day, isoDefaultLocalTime, formatTime

    • transformers

      • ReaderT, MonadIO (liftIO)

    • ​unliftio

      • UnliftIO.Exception, UnliftIO.Temporary (withSystemTempDirectory), UnliftIO.Async

    • stm

      • TVar

    • containers

      • Data.HashMap.Lazy

    • more...​​

Extra: Utility Compare Functions

  • Helpers for following slides, not important
$ export PREV_REV=478239fe85a97b3df8897516ebe64b6e0a9b0fe9;

$ pickndiff_file() {
> export FILE=$(git grep "$1" $PREV_REV | fzf | cut -f 2 -d :)
> if [ -z $FILE ]
>   then echo "No file choosen"
>   else
>     echo "Diffing $PREV_REV:$FILE..."
>     git diff $PREV_REV:$FILE master:$FILE | delta
> fi
> }

$ count_prev_occs() { git grep "$1" $PREV_REV | wc -l; }

Various Cleanup

  • error "blah" impureThrow $ flip StringException callStack “blah”

    • These are still partial, but now at least our impure exceptions are intentional and have a stack trace 🎉

  • Foundational AWS streaming utilities had redundant MonadReader, MonadCatch, Aws.HasEnv constraints

    • For some reason was being deferred with {-# OPTIONS_GHC -Wno-redundant-constraints #-}
  • Applied various HLint suggestions

GHC Features

GHC 8.6 Features

GHC 8.6.1 Release Announcement

GHC 8.6.1 Release Notes

 

  • Extensions
    • DerivingVia
    • QuantifiedConstraints
    • BlockArguments
  • ghci (repl) commands
    • :doc
  • Language
    • Valid Hole fits
  • ghc-heap-view packaged

DerivingVia

Reuse typeclass instances from other types

with the same shape

{-# LANGUAGE DerivingVia #-}

newtype VarChar255 = VarChar Text
  deriving (Eq, Show)
  deriving (SqlField) via SqlText
  deriving (Arbitrary) via UTF8UpToNCharacters 255

newtype RowCount = RowCount Int
  deriving (Eq, Show, Num)
  deriving (Semigroup, Monoid) via Sum Int

QuantifiedConstraints

Moar type level prolog

{-# LANGUAGE QuantifiedConstraints #-}

class (forall a. Functor (p a)) => Bifunctor p where...
 
class (forall a c. Functor (p a)) => Profunctor p where...

class (forall m. Monad m => Monad (t m)) 
    => MonadTrans t where
  lift :: m a -> t m a
class Bifunctor p where...
 
class Profunctor p where...

class MonadTrans t where
    lift :: (Monad m) => m a -> t m a

BlockArguments

Moar type level prolog

{-# LANGUAGE QuantifiedConstraints #-}

class (forall a. Functor (p a)) => Bifunctor p where...
 
class (forall a c. Functor (p a)) => Profunctor p where...

class (forall m. Monad m => Monad (t m)) 
    => MonadTrans t where
  lift :: m a -> t m a
class Bifunctor p where...
 
class Profunctor p where...

class MonadTrans t where
    lift :: (Monad m) => m a -> t m a

QuantifiedConstraints

Moar type level prolog

{-# LANGUAGE QuantifiedConstraints #-}

class (forall a. Functor (p a)) => Bifunctor p where...
 
class (forall a c. Functor (p a)) => Profunctor p where...

class (forall m. Monad m => Monad (t m)) 
    => MonadTrans t where
  lift :: m a -> t m a
class Bifunctor p where...
 
class Profunctor p where...

class MonadTrans t where
    lift :: (Monad m) => m a -> t m a

:doc Command

Get info on type/function in the REPL

$ ghci
...
λ> :doc Maybe
 The 'Maybe' type encapsulates an optional value.  A value of type
 @'Maybe' a@ either contains a value of type @a@ (represented as @'Just' a@),
 or it is empty (represented as 'Nothing').  Using 'Maybe' is a good way to
 deal with errors or exceptional cases without resorting to drastic
 measures such as 'Prelude.error'.

 The 'Maybe' type is also a monad.  It is a simple kind of error
 monad, where all errors are represented by 'Nothing'.  A richer
 error monad can be built using the 'Data.Either.Either' type.

Valid Hole Fits

Get suggestions for type holes

f :: String
f = _ "hello, world"
F.hs:2:5: error:
    • Found hole: _ :: [Char] -> String
    • In the expression: _
      In the expression: _ "hello, world"
      In an equation for ‘f’: f = _ "hello, world"
    • Relevant bindings include f :: String (bound at F.hs:2:1)
      Valid hole fits include
        cycle :: forall a. [a] -> [a]
        init :: forall a. [a] -> [a]
        reverse :: forall a. [a] -> [a]
        tail :: forall a. [a] -> [a]
        id :: forall a. a -> a
        (Some hole fits suppressed;
         use -fmax-valid-hole-fits=N
         or -fno-max-valid-hole-fits)

Aside: Wingman for HLS

Plugin for Hole-driven development

Example: Type-Aware Code generation

Aside: Wingman for HLS

Example: Case Splitting

Aside: Wingman for HLS

Example: Tactics Metaprogramming

ghc-heap-view packaged

Introspect the Haskell heap

λ> value = "A Value"
λ> x :: (String, Bool, [Bool]) = 
  ( value
  , if head value == 'A' then value else ""
  , cycle [True, False] 
  )
  
λ> :printHeap x
let x1 = _bco
    x21 = []
in (x1,_bco,_bco)

-- evaluate everything
λ> length (take 100 (show x)) `seq` return ()
λ> :printHeap x
let x1 = "A Value"
    x16 = True : False : x16
in (x1,x1,x16)

ghc-vis

ghc-heap-view enables tools like ghc-vis for visualizing the heap

λ> :{
ones = [1,1..]

at 0 (x:xs) = x
at n (x:xs) = at (n-1) xs
:}
λ> 

ghc-vis

ghc-heap-view enables tools like ghc-vis for visualizing the heap

λ> :{
ones = [1,1..]

at 0 (x:xs) = x
at n (x:xs) = at (n-1) xs
:}
λ> 
λ> at 1
λ> at 2
λ> at 3

ghc-vis

ghc-heap-view enables tools like ghc-vis for visualizing the heap

λ> :{
ones = [1,1..]

at 0 (x:xs) = x
at n (x:xs) = at (n-1) xs
:}
λ> 
λ> at 1
λ> at 2
λ> at 3

ghc-vis

ghc-heap-view enables tools like ghc-vis for visualizing the heap

λ> :{
ones = [1,1..]

at 0 (x:xs) = x
at n (x:xs) = at (n-1) xs
:}
λ> 
λ> at 1
λ> at 2
λ> at 3

GHC 8.8 Features

GHC 8.8.1 Release Announcment

GHC 8.8.1 Release Notes

Hie Files

HIE Files - coming soon to a GHC near you!

 

 

  • Persistent files for IDE that still works when code doesn't typecheck
  • Powers Go-To-Reference and more
  • Used by tools like
    • Stan for linting
    • hiedb for querying code facts from hie files  (needs ghc 8.10 mismatch preventing demo)

Hie Files

Go-To-Definition on non-local libraries

Visible Kind / Type-Level Type Applications

  • TypeApplications let us use type arguments at the term level.
    • Now we can specify kind arguments at the type level
λ> :set -XTypeApplications -XDataKinds
λ> import GHC.TypeLits

λ> :kind '[] @Nat
'[] @Int :: [Nat]
λ> :kind 'Just @Nat
'Just @Nat :: Nat -> Maybe Nat

GHC 8.10 Features

​GHC 8.10.1 Release Announcement

GHC 8.10.1 Release Notes

Migration Notes

 

Liquid Haskell Plugin

Refinement types as a GHC plugin

GHC 9.0 Features

GHC 9.0.1-rc Release Notes

 

 

  • Extensions
    • Linear Types
    • QualifiedDo
  • Improved Runtime and Compiler performance

QualifiedDo

someShortCircuitingFn :: Either SomeError SomeResult
someShortCircuitingFn = Either.do
  result1 <- doThingOrFail
  result2 <- doThingWithResult1OrFail result1
  pure result2

someAsyncComputation :: Async ()
someAsyncComputation = Async.do
  response1 <- makeAsyncRequest1
  response2 <- makeAsyncRequest2
  doSomething response1
  doSomething response2
  
printThings :: IO ()
printThings = IO.do
  input <- getLine
  print input
  
  

Explicitly qualify the monad for do-notation

Linear Types

  • Linear functions are regular functions that guarantee that they will use their argument exactly once.
data Multiplicity
  = One
  | Many
newtype ReleaseMap = ReleaseMap (IntMap (Linear.IO ()))

-- | The resource-aware I/O monad. This monad guarantees that 
-- acquired resources are always released.
newtype RIO a = RIO (IORef ReleaseMap -> Linear.IO a)
  deriving (Data.Functor, Data.Applicative) via (Control.Data RIO)

unRIO :: RIO a %1-> IORef ReleaseMap -> Linear.IO a
unRIO (RIO action) = action

GHC 9.2 Features

GHC 9.2.1-alpha2 Release Notes

 

GHC >9.2 Features

GHC Activities Report

From Well-Typed

Part III - State of Haskell Ecosystem @ TVision

and Beyond

Contents

  • State of...
    • TVision Haskell Dependencies
    • Wider Haskell Ecosystem
    • TVision Backend Codebase

 

Informal, opinionated, and conjectural considerations on scaling internal codebase and the ecosystem health

  • Scaling Observations
    • Build Times
    • Operational Overhead
    • Maintenance Burden
  • Risk Analysis
    • Producer of Haskell code
    • Consumer of Haskell code
  • Directions
    • ​Idealistic Scenarios and Recommendations
    • Possible Complications

Scaling Considerations

Some things to keep an eye on

Build Times

  • Why are build times a problem?
    • Anti-Iteration: Builds usually happen when you're trying to get something done with the code (primary focus)
      • Necessary but constant, unavoidable roadblock
    • Wasteful: (usually) failed builds = useful; successful builds = pointless
      • Consider impact of flaky builds 😱😱😱
    • Environmentally Responsible
  • Potential Improvements
    • ​Sufficiently smart rebuilds
      • Shared Build Cache
      • Faster build tools
    • Design for Fast Iteration
      • Disable non-functional branch checks, minimize feedback loop
      • Automate harmless refactorings - Linters, Formatters, Granular Opt-in, Local Tools, PR dependency bumps (trust-dependent)

Operational Overhead

  • Issues
    • We're constantly growing the volume and variety of interactions inside and adjacent to our backend
  • Potential Improvements
    • Observability
      • Distributed Tracing
      • ​Safe + Portable + Reproducible Infrastructure
    • Design for Users as Operators
      • Tooling: ​E2E developer environments, decoupled scheduling + execution, metric roles

Maintenance Burden

  • Issues
    • Usage and extensions of backend components beyond their original designs
      • Optimized for restatement-based data evolution rather than migration-based (tradeoffs)
    • Overhead impacts:
      • Feature Additions, evolvability, onboarding, regression testing
  • ​​Potential Improvements
    • Stricter requirements on dependencies
      • Static Linking
    • Stricter usage (non-)requirements
      • Backwards / Forward compatibility

State of TVision

Haskell Dependencies

Focus on internal maintenance of upstream Haskell packages

Core Dependencies

  • Standard Library
    • ClassyPrelude
  • Databases
    • Opaleye (used for modern projects)
    • Persistent/Esqueleto (legacy?)
  • Services + Clients
    • servant, servant-*
  • Scripts
    • optparse-applicative
  • ​Streaming
    • Conduit
  • Cloud
    • amazonka, amazonka-*
  • Code Generation
    • template-haskell
  • Records
    • lens
  • ​Application / Effects
    • mtl, transforms
    • lens (classy optics)
  • ​Tests
    • hspec
    • Quickcheck

Known Risks

  • ClassyPrelude
    • FPComplete Universe
  • amazonka

Build and Release Tools

  • Haskell Specific
    • Stack
      • GHC Extensions
      • Optimization Flags
    • Shake
    • hie-bios (Informal)
  • General ​Developer Setup
    • ​Package Manager
      • ​Homebrew
      • Apt

Extra Tools

  • Production Use
    • postgres-simple-migration
  • Developer Use
    • Static Analysis
      • HLint (informal)
      • HLS (informal)
      • Now .hie files (informal)
    • Formatter
      • Stylish
    • ​Docs
      • ​Dash

Amazonka

  • Bitrotted
    • Untouched for 2 years despite numerous attempts to help
    • Finally there's been activity in the last month
      • Supposedly many upcoming breaking changes
    • Alternatives
      • aws
        • GHC 8.8 support, last commit 03/2020
        • No opsworks, emr functionality
      • Shelling out to awscli / boto

State of Wider

Haskell Ecosystem

Focus on how it affects us

Haskell in Industry

Some companies (I know of) making making major contributions to Haskell ecosystem:

  • Github
  • Facebook
  • Hasura
  • FPComplete
  • Mercury
  • Tweag
  • IOHK
  • Google (GSoC)

More notable users of Haskell:

 

  • Tesla
  • Target
  • Barclays
  • Simspace
  • NoRedInk
  • CircuitHub
  • Chordify

Alive and Well!

Haskell Adoption

  • Easier than ever
    • Books
      • Production Haskell
      • Simple Haskell
    • Haskell Language Server
  • Usability Improvements
    • RecordDotSyntax, GHC2021
    • Haskell Foundation
  • Being taught in schools
    • Many UK schools
    • Carnegie Mellon? Cornell?

State of TVision

Backend Codebase

Focus on internal Haskell usage + libraries

Core Projects

  • Tracker
    • Tracker Service (and client)
    • Upload Manager Service (and client)
  • Device Config
    • Device Config Service
    • Photo Training Service (and exterior App)
  • Batch Ingestion
    • Programs - Tivo
    • Ads - Tam, Kinetiq
  • Asset Pipeline

Project Tech Debt

  • Tracker
  • Stale Projects
    • Device Config
      • Photo Training replaced by mobile app
    • Upload Manager
    • Zoho
    • Tivo (batch ingestion)
      • Here be dragons 👻👻
  • Unused Projects
    • Kantar, Sky ad imports
      • Note we do use ad-import-* for TAM ad imports

Sauron Diagnostic Report

  • Magnitude
    • ~300KLOC (75KLOC Haskell)
      • LOTS of config, (generated code too?)
    • # of modules, tests, etc ???
  • Build Times
    • Local
      • Clean Build - 8 minutes
      • Clean Build + Test - ????
    • CI
      • Build - 20 minutes
      • Dependency Change
        • Build Change (ex. Scripts): .3 hr + .3 hr =  40 min
  • Build Times cont..
    • CI cont..
      • Dependency Change cont..
        • Snapshot or System Config Change: 1.5 + .3 + .3 = ~2 hours
  • Build Occurences
    • # CI builds a week, month, year
    • # master CI build a week, month, year

Can we improve?

Definitely!

 

Should we improve?

🤷‍♂️

Ingest Tracker

  • Tracker
    • Addressing Tracker Pain Points
    • Improving Invocation DSL
      • Remote Invocation Infrastructure instead of using Driver
    • Tracker UI Improvments
    • Adding Commit Phase
  • Partial / Wholesale Replacement

TVision Backend

Kinetiq

VidVita

ACRCloud

Tivo

Custom

Asset Pipeline

TVision Backend

Kinetiq

VidVita

ACRCloud

Tivo

Custom

Future Asset Pipeline?

Revisiting Monorepo

Cons

 

We should definitely keep the monorepo, but there are tradeoffs:

  • Build Times
    • Compile Times
    • Static Analysis
    • Test Suite
  • Batched Upkeep
    • When there is horizontal organization
  • Sophisticated Build Scripts

Revisiting Monorepo

Pros

 

Monorepo offers advantages to consider leaning into:

  • Wholesale Tooling
    • Local Development
    • Integration Testing
  • Monolith (-ish)
    • Continuous Delivery
      • Staging?

 

Other things to look into

  • Immutable Infrastructure

State of

GHC Haskell Compiler

Focus on how it affects us

Haskell Upgrade

By Heneli Kailahi

Haskell Upgrade

  • 339