Good First Issues in Haskell
Explore curated starter issues in high-scoring Haskell repositories. Every listed issue belongs to a welcoming repository scored by C-Rank™ on PR merge rates, review responsiveness, and first-timer acceptance.
Arm Issue Sniper for fresh Haskell beginner issues
Beginner issues in welcoming S/A-Tier repositories get snatched within 15 minutes. Receive instant alerts on Discord, Telegram, or Email the minute a maintainer opens a new task.
Live Good First Issues (30)
If you hdb app/Main.hs and app/Main.hs doesn't exist, you get an error message about ghc --numeric-version failing. Instead, we should make sure the target exists before trying to set up and fail with an error message gracefully
In the course of chasing #3343 it became clear that bvEq_refl is missing from basic_ss. This is silly and should be fixed. (There are reasons to not necessarily unfold or rewrite equalities, which is why a lot of the equality-related rules are left out. But those reasons don't extend to reducing reflexivity.)
We use a naïve List.find when normalising categories, which is linear in complexity. We can probably use a Map instead.
We only use blue/red messages when starting the executables, we should vendor those.
Today, iso8601-time is only used for parseISO8601, but we already use iso8601ParseM from the time library elsewhere, so no need for an extra dep.
In the CircuitHub codebase we have: -- | @range \@> now()@: true when the current timestamp falls within the range. currentlyValid :: Expr (Range UTCTime) -> Expr Bool currentlyValid range = rawBinaryOperator "@>" range now we should upstream this or something similar, since this is something we want to quite often do with ranges.
Suppose you have these files: a/a.saw b/b.saw c/c.saw and suppose both b.saw and c.saw do include_once "../a/a.saw", and c.saw also includes b.saw. Then from the perspective of c.saw the path of a.saw is ../a/a.saw, but from the perspective of b.saw the path is ../b/../a/a.saw, because we try to make includes relative to where they're included from. But include_once just looks at the path strings, and they're different, so this doesn't work as intended. This does, however, seem like a reasonable thing to do and to want include_once to work for, so I guess we do need it to normalize paths for comparison... much as normalizing paths is annoying and generally wrong. (That or we could check the system-level file identity... but I'm not sure how you do that from Haskell or whether it's workable in Windows.) @qsctr
Currently, names produced in generated LLVM code are not consistently mangled to ensure they don't clash with one another or with user-written code. We should think through how much mangling is needed for this, and implement it consistently for all generated names in LLVM code.
Summary DateTime (core/core/DateTime.hs) derives ToJSON/FromJSON but has no ToSchema instance. Because Schema's generic derivation requires every field's type to have ToSchema, any record exposed through the web transport — a query read model registered via deriveQuery / withQuery, or any type deriving ToSchema — fails to compile the moment it contains a DateTime (or Maybe DateTime) field. Since DateTime is the natural type for timestamps carried on events and folded onto entities, this makes it impossible to surface a timestamp on an HTTP read model without a workaround (an orphan instance, or converting the field to Int epoch seconds / Text first). Reproduction import Core data Row = Row { id :: Uuid , occurredAt :: DateTime -- or `Maybe DateTime` } deriving (Generic) instance ToSchema Row -- ❌ No instance for `ToSchema DateTime` Maybe DateTime fails the same way — instance ToSchema (Maybe inner) needs ToSchema inner. Expected DateTime should have a ToS
Following the publication of Scarf moving away from Haskell, discussions mentioned that type-family-heavy code, like code using beam, can build slowly. I personally think that the tradeoff for this slower compilation is much, much more safety when interacting with the database. However, if we can preserve this safety and improve build times, we should! This ticket is about creating a benchmark for building a beam-based application. Improvements to build times can follow separately.
Please consider adding hadolint on winget.
MySQL remains a serious choice over Postgres in many domains. We should support it. There exists a beam-mysql, but it is unmaintained. beam-mysql is based on mysql, which is also unmaintained. We should: import beam-mysql in this repo; Switch over to use mysql-haskell, which is maintained I'm a little oversubscribed at the moment, but I'm happy to review pull requests!
The SAWScript typechecker and interpreter are built with -Wno-incomplete-uni-patterns and there's a long comment at the bottom of Typechecker.hs that explains why. Basically, there are a couple places that use irrefutable patterns with incomplete matches as a shortcut. These basically function as assertions that the omitted cases of the partial matches are not reached, which is fine until they are and then it turns out that the generated code throws UserError behind your back. That results in the following undesirable behavior: sawscript> let x = 3 saw-script/src/SAWScript/Typechecker.hs:2216:5-60: Non-exhaustive patterns in [(pat'', e1, s)] sawscript> IOW, it crashes but doesn't exit, and while it does print the Haskell source location, it doesn't print what the offending value was. This is not what we want; the handful of affected cases should be turned into explicit panics that print useful messages. We can afford a couple extra lines of code for that in a project the size of SA
Example: { "type": "package", "platform": "common", "name": "example/package", "license": "BSD-3-Clause", "summary": "this summary line is not that long... but it is longer than expected at 80 chars", "source-directories": [ "src" ], "version": "1.0.0", "exposed-modules": [ "Main" ], "gren-version": "0.6.0 <= v < 0.7.0", "dependencies": { "gren-lang/core": "7.4.0 <= v < 8.0.0" } } "gren make" produces: gren: DecodeProblem "{\"command\":\"make\",\"optimize\":false,\"sourcemap .... <snipped> \"summary\":\"this summary line is not that long... but it is longer than expected at 80 chars\" <snipped> (Field "project-outline" (Field "summary" (Failure (Region (Position 1 266) (Position 1 348)) InvalidInput))) CallStack (from HasCallStack): error, called at terminal/Main.hs:30:13 in gren-0.6.5-inplace-gren:Main HasCallStack backtrace: collectBacktraces, called at libraries/ghc-internal/src/GHC/Internal/Excep
Many stream primitives can probably be rewritten with Selective since they only bind on a case distinction.
Our current default warning and language settings are a bit ridiculous. Lots of things are globally on for no reason, lots of things are globally off for no reason, and there's no reasonable structure to what is on or off. The Cabal file should be organized better, and the defaults should be improved.
When you run saw --help, it returns exit code 2: $ saw --help <snip> $ echo $? 2 This is a bit unfortunate, as I sometimes use saw --help as a basic smoke test to verify if saw can run at all on a given machine, but the non-zero exit code makes scripts believe that saw --help isn't working as expected. Any objections to changing the behavior to return exit code 0 instead?
Right now it is specialised to lists: concatS :: (Monad m) => StreamT m [a] -> StreamT m a I believe it could be: concatS :: (Monad m, Foldable t) => StreamT m (t a) -> StreamT m a Same for the automaton version.
Someone has kindly prepared support for Zed in the Zed Haskell Extension. It'd be good to reference it from the readme (Along with support for VScode, vim, emacs)
Description (-) and negate over Natural throw Underflow on negative results. fromInteger 0 correctly rejects zero, but 1 - 2 :: ContestationPeriod silently produces a runtime exception rather than a typed failure. The on-chain protocol assumes cp > 0; the type intends to enforce this but exposes ways to violate it that panic instead of returning Maybe. Location: hydra-tx/src/Hydra/Tx/ContestationPeriod.hs:28-38. Verification Read the Num instance; both operators delegate to Natural which throws on underflow. Suggested fix Remove the Num instance, or implement (-) and negate as error "use fromNominalDiffTime". Only legitimate constructors are fromInteger >0 and fromNominalDiffTime.
Description UTxO.fromList $ zip (txIns' tx) outputs zips the transaction's spent inputs with the freshly-produced decommit outputs. The resulting TxIn keys reference the head/fee-input UTxOs, not the new outputs. Latent bug since downstream consumers only read outputsOfUTxO, but any indexer/client touching the TxIns sees ghost references. Location: hydra-tx/src/Hydra/Tx/Decrement.hs:130-137. Verification Read the observation builder; compared with observeFanoutTx/observeFinalPartialFanoutTx which correctly use mkTxIn tx <$> [0..]. Suggested fix Replace inputs = txIns' tx with inputs = mkTxIn tx <$> [1..] (decommit outputs start at index 1; head output is index 0).
Description (depositIn, _) = List.head $ UTxO.toList depositScriptUTxO. If a caller passes an empty depositScriptUTxO (e.g. lookup failure), the node aborts at tx-construction time with Prelude.head: empty list. The comment guarantees "single output" but the code silently ignores extras. Location: hydra-tx/src/Hydra/Tx/Increment.hs:103. Verification Read the call site; List.head is unguarded. Suggested fix Take the deposit input as (TxIn, TxOut CtxUTxO) directly (mirroring the head-input convention), or return Either Text Tx. At minimum, pattern-match and error with a useful message.
Description cardanoChainConfigParser allows hydraScriptsTxId = [] (the many alternative succeeds with empty). validateNetwork checks only the literal network name; nothing requires --network to match --mainnet/--testnet-magic. Misconfigurations fail far from the CLI line (queryScriptRegistry []) or silently submit to the wrong chain. Location: hydra-node/src/Hydra/Options.hs:459-481, 795-809, 935-944. Verification Read the parser chain and validateRunOptions; neither check exists. Suggested fix Add Cardano … && null hydraScriptsTxId → Left MissingHydraScriptsTxId. Require --network mainnet paired with the Mainnet networkId (and similarly for preview/preprod via magic numbers).
Description withMonitoring calls serveMetrics (fromIntegral monitoringPort) ["metrics"] (sample registry), which runs Warp on the wildcard host. By contrast --api-host defaults to 127.0.0.1. Operators expecting localhost-only consistency don't get it; head-related counters (snapshot counts, tx counts) leak to anyone who can reach the port. Location: hydra-node/src/Hydra/Options.hs:711-720, Logging/Monitoring.hs:46-55. Verification Read withMonitoring; no setHost applied. CLI has no --monitoring-host option. Suggested fix Add a --monitoring-host option (default 127.0.0.1) symmetric with --api-host, threaded into Logging.Monitoring to apply Warp.setHost.
Description Authenticated msg = { payload, party }. Signatures are discarded after verify. wireNetworkInput feeds only (party, msg) into HeadLogic; persistence stores ReceivedMessage{sender, msg}. If a head later disputes whether peer X actually authored a particular AckSn, no on-disk evidence remains to re-prove authorship to a third party. Location: hydra-node/src/Hydra/Network/Authenticate.hs:31-35, Hydra/Node.hs:240-247. Verification Read the Authenticated definition and ReceivedMessage persistence; signature is not carried. Suggested fix Keep the signature in Authenticated (or store the raw Signed blob in the event log) so audit/replay analysis can verify authorship after the fact.
Description mkThreadId = fromMaybe 0 . readMaybe . Text.unpack . Text.drop 9 . show relies on show of a ThreadId matching "ThreadId N". If GHC ever changes the Show instance (or in IOSim-style monads), every log line gets threadId: 0 and traces from different threads become indistinguishable. Location: hydra-node/src/Hydra/Logging.hs:154-162. Verification Read the parse expression; brittle to upstream string-format changes. Suggested fix On parse failure, hash the displayed form (or use the displayed string verbatim) to keep cross-thread distinguishability. Alternatives considered Use myThreadId from a MonadThread abstraction: bigger refactor.
Description signableBytes is excluded from JSON encoding and reconstructed via mkSeenSnapshot on decode. No test asserts that after a JSON roundtrip the resulting SeenSnapshot still accepts the same AckSn signatures as before. An accidental divergence between getSignableRepresentation snapshot at sign-time vs decode-time would silently brick snapshot confirmation post-restart. Location: hydra-node/src/Hydra/HeadLogic/State.hs:166-210. Verification Read the manual ToJSON/FromJSON/mkSeenSnapshot; no roundtrip property test exists in the suite. Suggested fix Add a property test (Aeson.decode . Aeson.encode = id) plus a signableBytes == getSignableRepresentation snapshot assertion in test/Hydra/HeadLogic/StateSpec.hs.
llvm_verify, jvm_verify, mir_verify (as well as llvm_refine_spec) check the detectVacuity option and call Vacuity.checkAssumptionsForContradictions. There is no trace of this in the x86 code. That means if someone requests vacuity checking, it silently won't happen, which could end up being a quite expensive/frustrating detour for someone.
Top Haskell Repositories with Beginner Issues (0)
Updated Daily via GitHub GraphQLHow to make your first Haskell open-source pull request
Finding approachable Good First Issues in Haskell allows you to build real-world software engineering experience. Instead of submitting PRs to abandoned repositories, GetMerged verifies maintainer review speeds and first-timer acceptance rates before you write a single line of code.
Select any issue above to claim it directly on GitHub, or click a repository to inspect full maintainer review turnaround metrics and triage guidance!
Get this week's top welcoming repos + fresh Good First Issues for Haskell
Free weekly email, scoped to Haskell. No account needed - confirm once and unsubscribe anytime.