diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,21 @@
 # Revision history for railroad
 
+## 0.3.0.0 -- unreleased
+* The test suite and CI now run on GHC 9.10, 9.12 and 9.14 (`tested-with`). The dependency bounds are
+  unchanged, so older GHCs are still allowed but are no longer tested.
+* Breaking: `Railroad.Cardinality` is merged into `Railroad.Bifurcate`. `CardinalityError` and
+  `cardinalityErr` are exported from `Railroad.Bifurcate`, and still from `Railroad` and
+  `Railroad.MonadError`.
+* New classifiers, each a plain function into `Either`: `neg` (swap failure and success),
+  `tagBy` (the classifier behind `?>`), `atLeastOne`, `exactlyOne` and `noneAtAll` (behind `?+`,
+  `?!` and `?∅`), `partitionBifurcate`, `successes`, `failures` and `anySucceeds`. Any verb applies to a classified value:
+  `xs <&> exactlyOne ??~ f`, `xs <&> anySucceeds ?<* h ? e`.
+* New operator `(?∃)`: succeeds if at least one element succeeds, returning the successes, and
+  otherwise throws an error built from every failure. `xs ?∃ f` is `xs <&> anySucceeds ?? f`.
+* `(?>)`, `(?+)`, `(?!)` and `(?∅)` are now defined over the classifiers. Their types and behaviour
+  are unchanged.
+* `CardinalityError` derives `Eq` and `Show`.
+
 ## 0.2.1.0 -- 2026-10-11
 * Add the tap operator `(?<*)`: runs an effect on the failure and passes the original value through
   unchanged, so a failure can be logged or counted before a constant error throws it away:
diff --git a/railroad.cabal b/railroad.cabal
--- a/railroad.cabal
+++ b/railroad.cabal
@@ -1,22 +1,22 @@
 cabal-version:      3.0
 name:               railroad
-version:            0.2.1.0
+version:            0.3.0.0
 license:            BSD-3-Clause
 license-file:       LICENSE
 author:             Frederik Kallstrup Mastratisi
 maintainer:         mastratisi@proton.me
 category:           Control, Error Handling
 build-type:         Simple
-tested-with:        GHC == 9.4.8, GHC == 9.6.7, GHC == 9.8.4, GHC == 9.10.3, GHC == 9.12.4, GHC == 9.14.1
+tested-with:        GHC == 9.10.3, GHC == 9.12.4, GHC == 9.14.1
 extra-doc-files:    CHANGELOG.md, readme.md
-synopsis:           Railway-oriented error handling: unwrap or throw in one short operator.
+synopsis:           Railway-oriented error handling: unwrap, throw, recover or fall back
 description:
     Railway-oriented error handling for Haskell: one short operator per
     step, and the failure paths stop taking up lines.
 
-    Each operator unwraps one layer of a result (@Maybe@, @Either@, @Bool@,
-    @Validation@, or a collection of them) and says what to throw if it
-    failed. Where you would write
+    Each operator looks at a result (@Maybe@, @Either@, @Bool@,
+    @Validation@, or a collection of them), decides whether it is a failure,
+    and says what to do about it. Where you would write
 
     > rows <- runQuery (userById uid) >>= either (const (throwError err503)) pure
     > user <- case rows of
@@ -31,30 +31,35 @@
     Layers are unwrapped left to right: @? err503@ unwraps the database
     @Either@, then @?!@ demands exactly one row (none is a 404, several a 500).
 
-    * Works in any @MonadError@ ("Railroad.MonadError": @mtl@, @ExceptT@,
-      servant's @Handler@) and in @effectful@ with the @Error@ effect
-      ("Railroad").
+    What to do about a failure:
 
-    * Unwrap or throw: @?@ (constant error) and @??@ (error computed from
-      the failure). Recover instead of throwing: @?~@ and @??~@.
+    * Throw: @?@ (constant error) and @??@ (error computed from the failure).
 
-    * Tag by a predicate with @?>@, or fall back to another source with @?|@
-      and @?|<>@.
+    * Recover: @?~@ and @??~@.
 
-    * Log a failure before a constant error discards it, with @?\<*@:
-      @runQuery q ?\<* logDbError ? err503@.
+    * Observe, then carry on: @?\<*@, as in @runQuery q ?\<* logDbError ? err503@.
 
-    * @Validation@ failures accumulate: validate a whole request and report
-      every problem at once.
+    * Fall back to another source: @?|@, or @?|<>@ to combine the errors.
 
-    * All operators are @infixl 1@ like @>>=@ and @\<&\>@, so they chain without
-      parentheses.
+    What counts as a failure:
 
-    Collections are checked with quantifiers: @?@ is ∀ ("every element
-    succeeds", so an empty collection passes), @?+@ is ∃ ("at least one"),
-    @?!@ is ∃! ("exactly one") and @?∅@ is ¬∃ ("none"). They chain:
-    @xs ?+ e1 ? e2@ means "at least one, and all succeed". The readme has the
-    details and a longer servant example.
+    * @Nothing@, @Left@, @False@ and @Failure@. A collection fails if any
+      element does, so an empty collection passes. @Validation@ failures
+      accumulate, so a whole request can report every problem at once.
+
+    * Collections are also checked by size: @?+@ (at least one), @?!@ (exactly
+      one), @?∅@ (none), and @?∃@ (at least one element succeeded; keeps the
+      successes).
+
+    * Classifiers: @?>@ tags by a predicate. Any function into @Either@,
+      @Maybe@, @Bool@ or @Validation@ is a classifier, applied with @\<&\>@.
+      "Railroad.Bifurcate" provides @neg@, @atLeastOne@, @exactlyOne@,
+      @noneAtAll@ and @anySucceeds@.
+
+    Works in any @MonadError@ ("Railroad.MonadError": @mtl@, @ExceptT@,
+    servant's @Handler@) and in @effectful@ with the @Error@ effect
+    ("Railroad"). All operators are @infixl 1@ like @>>=@ and @\<&\>@, so
+    they chain without parentheses. The readme has more examples.
 homepage:     https://github.com/mastratisi/railroad
 bug-reports:  https://github.com/mastratisi/railroad/issues
 source-repository head
@@ -73,7 +78,6 @@
     import:           stuff
     exposed-modules:  Railroad
                       Railroad.Bifurcate
-                      Railroad.Cardinality
                       Railroad.MonadError
     hs-source-dirs:   src
     default-language: GHC2021
diff --git a/readme.md b/readme.md
--- a/readme.md
+++ b/readme.md
@@ -1,7 +1,10 @@
 # railroad
 
-**A terse railway-oriented error handling DSL for Haskell. Unwrap `Maybe`, `Either`, `Bool` or `Validation`, or throw a mapped error, with one short operator.**
+**Railway-oriented error handling for Haskell. Unwrap a `Maybe`, `Either`, `Bool` or `Validation`, or throw a mapped error, with one short operator.**
 
+[![Haskell](https://img.shields.io/badge/language-Haskell-%23612a7e.svg)](https://www.haskell.org/)
+[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
+
 Instead of
 
 ```haskell
@@ -18,15 +21,12 @@
 user <- runQuery (userById uid) ? err503 ?! cardinalityErr err404 (const err500)
 ```
 
-Here `runQuery` returns `Eff es (Either DbError [row])`. Each operator
-unwraps one layer (the database failure, then "exactly one row") and says
-what to throw if it fails, so the happy path stays one line. A longer
-[servant example](#example-a-servant-backend) is below.
+`runQuery` returns `Eff es (Either DbError [row])`. Each operator unwraps one
+layer, left to right: `? err503` unwraps the database `Either`, then `?!`
+demands exactly one row (none is a 404, several a 500).
 
-The purpose of `railroad` is to error handle tersely, keeping the
-happy-path clean of control-flow and functor unpacking.  It does this
-with 11 combinators, that abstract over binary co-products (`Bool`,
-`Maybe`, `Either`, `Validation`) and Traversables or Folds over them.
+It works in any `MonadError` (`mtl`, `ExceptT`, servant's `Handler`) and in
+`effectful` with the `Error` effect.
 
 ```haskell
 import Railroad.MonadError
@@ -34,63 +34,76 @@
 
 example :: Either String Int
 example = runExcept $ do
- -- False ? "error" would make the block short circuit to Left "error"
- 
- x <- pure (Just 2) ? "Value missing"
- 
- -- Right (Just 1) ~> Just 1 ~> 1
- y <- pure (Right $ Just 1) ? "Outer fail" ? "Inner fail"
- 
- -- [Just 4] ~> [4] ~> 4
- z <- pure [Just 4] ? "List failed" ?! const "Not a single element"
- 
- q <- pure Nothing ?~ 1 
- 
- pure (x + y + z + q) -- ~> Right 8 
+  x <- pure (Just 2) ? "Value missing"                          -- Just 2        ~> 2
+  y <- pure (Right (Just 1)) ? "Outer fail" ? "Inner fail"      -- Right (Just 1) ~> Just 1 ~> 1
+  z <- pure [Just 4] ? "List failed" ?! const "Not one element" -- [Just 4]      ~> [4] ~> 4
+  q <- pure Nothing ?~ 1                                        -- Nothing       ~> 1
+  pure (x + y + z + q)                                          -- Right 8
 ```
 
-You just need to work in a monad which supports a `MonadError`-like
-context. Either by being an instance of `MonadError` or in an Eff
-stack with `Error`.
+## How it works
 
-[![Haskell](https://img.shields.io/badge/language-Haskell-%23612a7e.svg)](https://www.haskell.org/)
-[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
+Every operator does two things: it decides whether a value is a failure, then it says what
+to do about a failure. Operators read left to right, and each one unwraps one layer:
 
----
+```
+runQuery (userById uid)           Eff es (Either DbError [User])
+  ? err503                        Left _  → throw err503          Right rows → rows :: [User]
+  ?! cardinalityErr err404 (const err500)
+                                  [] → throw err404, several → throw err500
+                                  [u] → u :: User
+```
 
-## Modules
+What counts as a failure depends on the type: `Nothing`, `Left`, `False` or `Failure`. A
+list fails if any element does. There are four things to do about a failure:
 
-- `Railroad`: the operators for the `effectful` `Error` effect.
-- `Railroad.MonadError`: the same operators for any `MonadError` (`mtl`, `ExceptT`, servant's `Handler`).
-- `Railroad.Bifurcate`: the `Bifurcate` class and the operators that never throw
-  (`?>`, `?~`, `??~`, `?|`, `?|<>`, `?<*`); `Railroad.Cardinality`: `CardinalityError`.
-  Both of the above re-export these, so you rarely import them directly.
+| Do | Operators | Example |
+|---|---|---|
+| throw an error | `?` `??` | `action ? err503` |
+| recover with a default | `?~` `??~` | `action ?~ 0` |
+| run an effect, then carry on | `?<*` | `action ?<* logIt ? err503` |
+| try another source | `?\|` `?\|<>` | `cache k ?\| db k ? NotFound` |
 
+The operators for collections (`?+`, `?!`, `?∅`, `?∃`) decide by how many elements there
+are or succeeded; the next sections go through them. The operators also read as logical
+connectives and quantifiers (∧, ∨, ¬, ∀, ∃); that reading is in [`railroad.md`](railroad.md).
+
+## Contents
+
+- [How it works](#how-it-works)
+- [Install](#install)
+- [Example: a servant backend](#example-a-servant-backend)
+- [The operators](#the-operators)
+- [What counts as an error](#what-counts-as-an-error)
+- [Gotcha: an empty collection passes](#gotcha-an-empty-collection-passes)
+- [Behavior under an evolving code base](#behavior-under-an-evolving-code-base)
+- [Exercises](#exercises)
+- [Modules](#modules)
+
 ## Install
 
+### Hackage
+
+The package is called `railroad`: https://hackage.haskell.org/package/railroad
+
 ### Nix
 
-Here is an expression you can add to `ghcWithPackages`:
+An expression for `ghcWithPackages`:
+
 ```nix
-railroad = pkgs.haskellPackages.callCabal2nix "railroad" (pkgs.fetchFromGitHub 
+railroad = pkgs.haskellPackages.callCabal2nix "railroad" (pkgs.fetchFromGitHub
   { owner  = "mastratisi"
   ; repo   = "railroad"
   ; rev    = "master"
-  ; sha256 = pkgs.lib.fakeSha256; # Nix will tell you the real hash on first build   
+  ; sha256 = pkgs.lib.fakeSha256; # Nix prints the real hash on the first build
   }) {};
 ```
 
-### Hackage
-The package is under the name `railroad`. Link: https://hackage.haskell.org/package/railroad
-
-
-
 ## Example: a servant backend
 
-Handlers in a servant + `effectful` backend (adapted from a real one)
-are chains of queries and checks where each failure is an HTTP status.
-The queries return `Eff es (Either DbError [row])`: a database failure
-wrapping the rows. With `railroad`:
+Handlers in a servant + `effectful` backend (adapted from a real one) are
+chains of queries and checks where each failure is an HTTP status. The queries
+return `Eff es (Either DbError [row])`.
 
 ```haskell
 getUser :: Error ServerError :> es => UserId -> Eff es UserDTO
@@ -105,14 +118,12 @@
 
 updateAsset :: Error ServerError :> es => UserId -> Asset -> Eff es ()
 updateAsset uid asset = do
-  -- permitted is an SQL exists, so it always returns exactly one row (see the gotcha below)
+  -- `permitted` is an SQL exists: one row, so no empty-collection gotcha (see below)
   _ <- runQuery (permitted uid asset) ? err500 ? err403   -- db error, then "not permitted"
   ...
 ```
 
-Each operator unwraps one layer, left to right: `? err503` unwraps the
-`Either`, then `?!` demands exactly one row (none is a 404, several a
-500). The same handlers in plain Haskell:
+The same handlers in plain Haskell:
 
 ```haskell
 getUser' uid = do
@@ -134,172 +145,251 @@
   ...
 ```
 
-The failure paths no longer take up lines, and the order of the layers is
-the order you read them in. Validating a request with a `Validation`
-reports every problem at once: `pure (map validateFile files) ?? badRequest`
-turns all the invalid files into one 400. `Railroad.MonadError` has the
-same operators for plain `MonadError`, including servant's `Handler`.
-
-## Understanding the Operators
-
-| Operator | Purpose                                       | Example                       |
-|----------|-----------------------------------------------|-------------------------------|
-| `??`     | Derail on error with custom error mapping     | `action ?? toMyError`         |
-| `?`      | Derail with constant error                    | `action ? MyError`            |
-| `?>`     | Tag by predicate (`Left` carries the value)   | `action ?> isGood ? NotGood`  |
-| `??~`    | Recover with a mapped default (error → value) | `action ??~ toDefaultVal`     |
-| `?~`     | Recover with a const default value            | `action ?~ defaultVal`        |
-| `?+`     | Derail on empty collection                    | `items ?+ NoResults`          |
-| `?!`     | Derail if not exactly one element             | `items ?! fromCardinalityErr` |
-| `?∅`     | Derail on non-empty collections (alias `?@`)  | `items ?∅ DuplicateFound`     |
-| `?\|`    | Fall back to another action on error         | `cache k ?\| db k ? NotFound` |
-| `?\|<>`  | Fall back, combining the errors with `<>`     | `a ?\|<> b ?? toErr`         |
-| `?<*`    | Run an effect on a failure, then carry on     | `action ?<* logIt ? MyError`  |
-
-
-All operators are `infixl 1`, like `>>=`, `<&>` and `&`, so they chain
-left to right with them without parentheses:
-`runQuery q ? err503 <&> toDTO`.
-
-For the semantics of the operators there are 3 relevant questions:
-What counts as an error?  What happens in the error case?  What
-happens in the success case?
-
+## The operators
 
-### What counts as an error?
-Here we can split the operators into two families. Those that
-interpret errors on the structure of Functors and Traversable of
-Functor. Those that interpret error on the cardinality of Foldables.
+| Role | Operator | What it does | Example |
+|---|---|---|---|
+| Throw | `?` | Unwrap, or throw a constant | `action ? MyError` |
+| | `??` | Unwrap, or throw an error computed from the failure | `action ?? toMyError` |
+| Collections | `?+` | Throw on an empty collection | `items ?+ NoResults` |
+| | `?!` | Throw unless there is exactly one element | `items ?! fromCardinalityErr` |
+| | `?∅` (alias `?@`) | Throw on a non-empty collection | `items ?∅ DuplicateFound` |
+| | `?∃` | Throw unless at least one element succeeded; keep the successes | `sources ?∃ toErr` |
+| Recover | `?~` | Recover with a constant | `action ?~ defaultVal` |
+| | `??~` | Recover with a value computed from the failure | `action ??~ toDefault` |
+| Classify | `?>` | Tag by a predicate: a rejected value becomes the failure | `action ?> isGood ? NotGood` |
+| Observe | `?<*` | Run an effect on a failure, then carry on | `action ?<* logIt ? MyError` |
+| Fall back | `?\|` | Try the next source on failure; keep the last error | `cache k ?\| db k ? NotFound` |
+| | `?\|<>` | The same, combining all the errors with `<>` | `a ?\|<> b ?? toErr` |
 
-For the former, they all (`?`, `??`, `?~`, `??~`) interpret what
-an error is the same way:
+All operators are `infixl 1`, like `>>=`, `<&>` and `&`, so they chain left to
+right with them without parentheses: `runQuery q ? err503 <&> toDTO`. `<$>` binds
+tighter (infixl 4), so `f <$> action ? err` applies `f` to the wrapped value before
+it is unwrapped, and `f` receives the `Either`. Map the result with `<&>` after the
+operator.
 
- | Functor          | Error Constructor    | Error Info (`CErr`) | Result Info (`CRes`) |
- |------------------|----------------------|---------------------|----------------------|
- | `Bool`           | `False`              | `()`                | `()`                 |
- | `Maybe a`        | `Nothing`            | `()`                | `a`                  |
- | `Either e a`     | `Left`               | `e`                 | `a`                  |
- | `Validation e a` | `Failure`            | `e`                 | `a`                  |
- | `t f`            | Any element is error | `CErr f`            | `t (CRes f)`         |
+### Classifiers
 
-An empty `t f` has no element to fail, so it succeeds: see [the gotcha](#gotcha-an-empty-collection-passes).
+A classifier is any function whose result type the operators already understand:
+`Either e r`, `Maybe r`, `Bool`, `Validation e r`, or a traversable of those. It
+sorts a value into failure or success and says nothing about what to do with a
+failure. In `Either e r` the failure carries the reason (`e`) and the success carries
+the value you continue with (`r`). A `Maybe` loses the reason
+and a `Bool` loses the value.
 
+Existing functions are classifiers: `lookup k`, `readMaybe`, or a parser such as
+`parseAge :: String -> Either String Int`. Apply one with `<&>` and finish with an
+operator, which decides what happens on failure:
 
-For the latter each operator (`?+`, `?!`, `?∅`) interpret what
-cardinalities of a Foldable counts as errors differently. To wit:
+```haskell
+readBody <&> parseAge ?? BadRequest            -- throw, built from the parse error
+readBody <&> parseAge ??~ const 18             -- recover with a default
+readBody <&> parseAge ?<* logIt ? BadAge       -- log the parse error, then throw
+```
 
-| Operator    | Success Condition   | Error Info               | Result Info |
-|:------------|:--------------------|:-------------------------|:------------|
-| `?+`        | Non-empty           | `()`                     | `t a`       |
-| `?!`        | Exactly one element | `CardinalityError (t a)` | `a`         |
-| `?∅` / `?@` | Is empty            | `t a`                    | `()`        |
+The classifiers in the library are pure functions in `Railroad.Bifurcate` and need no
+monad:
 
-Where
 ```haskell
--- | Structurally a @Maybe ta@ for cardinality failures
-data CardinalityError ta = IsEmpty | TooMany ta
-
--- | Catamorphism for CardinalityError
-cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e
+neg         :: Bifurcate a => a -> Either (CRes a) (CErr a)   -- swap success and failure
+tagBy       :: (a -> Bool) -> a -> Either a a                 -- Right when the predicate holds
+atLeastOne  :: Foldable t => t a -> Maybe (t a)
+exactlyOne  :: Foldable t => t a -> Either (CardinalityError (t a)) a
+noneAtAll   :: Foldable t => t a -> Either (t a) ()
+successes   :: (Foldable t, Bifurcate a) => t a -> [CRes a]
+failures    :: (Foldable t, Bifurcate a) => t a -> [CErr a]
+anySucceeds :: (Foldable t, Bifurcate a) => t a -> Either [CErr a] [CRes a]
 ```
 
-### What happens in the error case?
-When an error is detected, the railroad "derails":
+Any classifier combines with any of four verbs. Only the throw column has dedicated
+operators (`m` is the action, `n` another source, `e` a constant error, `f` a mapper,
+`d` a default, `h` an effect):
 
-1. The provided error-mapping function is applied to the **Error
-   Info** (`CErr` or `CardinalityError`).
-2. The result of the error mapping is thrown
-3. This causes early termination of the monadic sequence.
+| Classifier | throw | recover | observe | fall back |
+|---|---|---|---|---|
+| structural (∀ on a collection) | `m ? e` `m ?? f` | `m ?~ d` `m ??~ f` | `m ?<* h` | `m ?\| n` |
+| `tagBy` | `m ?> p ? e` | `m ?> p ?~ d` | `m ?> p ?<* h` | `m ?> p ?\| n` |
+| `atLeastOne` | `m ?+ e` | `m <&> atLeastOne ?~ d` | `m <&> atLeastOne ?<* h` | `m <&> atLeastOne ?\| n` |
+| `exactlyOne` | `m ?! f` | `m <&> exactlyOne ??~ f` | `m <&> exactlyOne ?<* h` | `m <&> exactlyOne ?\| n` |
+| `noneAtAll` | `m ?∅ f` | `m <&> noneAtAll ??~ f` | `m <&> noneAtAll ?<* h` | `m <&> noneAtAll ?\| n` |
+| `anySucceeds` | `m ?∃ f` | `m <&> anySucceeds ??~ f` | `m <&> anySucceeds ?<* h` | `m <&> anySucceeds ?\| n` |
+| `neg` | `m <&> neg ? e` | `m <&> neg ??~ f` | `m <&> neg ?<* h` | `m <&> neg ?\| n` |
 
-**Recovery Operators (`?~`, `??~`):** Unlike the other operators,
-these do **not** derail the computation. Instead, they provide a
-"switch" to bring the logic back onto the happy path by providing a
-default value through a recovery function.
+A fall-back chain is finished with `?` or `??` to turn a total failure into a throw. A
+failure with several cases is `m ?? \case ...`. Nothing covers a success that also
+carries errors (`These`).
 
+`neg` expresses "must not exist" on a single value: `findByEmail e <&> neg ? err409`
+throws `err409` when a user is found and passes on `Nothing`. `?∃` is "at least one
+of these sources answered": it throws when all of them failed and returns the
+successes otherwise.
 
-**Traversal case:** For `Bool`, `Maybe`, `Either` the error
-accumulation is short-circuiting and stops at the first error. For 
-`t (Validation e a)` the semigroup constraint on `e` is used to
-accumulate all errors together. For more information on accumulating
-errors, see the
-[validation](https://hackage.haskell.org/package/validation/docs/Data-Validation.html)
-package on Hackage.
-### What happens in the success case?
-In the success case the result is unwrapped or validated (`?+`, `?!`)
-and the monad continues its execution. Result Info in the above tables
-shows the resulting type inside the monad.
+### Fallbacks
 
-### Fallback operators
-`?|` and `?|<>` try one source after another and stop at the first
-success. They are for sources that may be different structures (for
-example a cache returning `Maybe` and a database returning `Either`)
-with the same success type (`CRes`). The result is bifurcatable, so a
-final `?` or `??` turns a total failure into a throw:
+`?|` and `?|<>` try one source after another and stop at the first success. The
+sources may be different structures (a cache returning `Maybe`, a database
+returning `Either`) with the same success type. The right-hand side only runs if
+the left one failed. The result is bifurcatable, so a final `?` or `??` turns a
+total failure into a throw:
 
 ```haskell
 cache k ?| db k ?| legacy k ? NotFound
 ```
 
-`?|` keeps the last error. `?|<>` also needs the sources to share the
-error type (`CErr`) and combines all the errors with `<>`, in source
-order, when every source fails. Map differing error types to a common one
-first, for example with `note`-like functions:
+`?|<>` needs the sources to share an error type and combines all the errors with
+`<>`, in source order. Map differing error types to a common one first:
 
 ```haskell
 (cache k <&> note [CacheMiss]) ?|<> (db k <&> first pure) ?? NotFound
 ```
 
-Neither throws by itself, and the right-hand side only runs if the left
-one failed. Unlike `Validation`, which runs all independent checks and
-collects every failure, a fallback chain succeeds on the first good answer.
+`Validation` runs all independent checks and collects every failure. A fallback
+chain succeeds on the first good answer.
 
 ### Observing failures
-A constant error such as `? err503` throws the underlying failure away.
-`?<*` runs an effect on the failure first and passes the value through
-unchanged, so the next operator still sees it:
 
+`? err503` throws the underlying failure away. `?<*` runs an effect on the failure
+first and passes the value through unchanged, so the next operator still sees it:
+
 ```haskell
 runQuery q ?<* logDbError ? err503
 ```
 
-The handler is a function of the failure (`CErr`), returns `m ()`, and
-runs only when there is a failure. Like `<*` it keeps the left value, and
-like the other operators in `Railroad.Bifurcate` it never throws.
+The handler takes the failure (`CErr`), returns `m ()`, and runs only on a
+failure. It never throws.
 
+## What counts as an error
+
+The operators `?`, `??`, `?~`, `??~`, `?>`, `?|` and `?<*` agree on what a failure
+is, through the `Bifurcate` class:
+
+| Type | Failure | Failure info (`CErr`) | Success info (`CRes`) |
+|---|---|---|---|
+| `Bool` | `False` | `()` | `()` |
+| `Maybe a` | `Nothing` | `()` | `a` |
+| `Either e a` | `Left` | `e` | `a` |
+| `Validation e a` | `Failure` | `e` | `a` |
+| `t f` | any element fails | `CErr f` | `t (CRes f)` |
+
+On a traversable of `Bool`, `Maybe` or `Either` the check stops at the first
+failure. On `t (Validation e a)` the `Semigroup` on `e` collects all of them, so
+`pure (map validateFile files) ?? badRequest` turns every invalid file into one 400.
+An empty `t f` has no element to fail, so it succeeds (see the gotcha below).
+
+The collection operators `?+`, `?!` and `?∅` look at the number of elements:
+
+| Operator | Succeeds when | Failure info | Success info |
+|:---|:---|:---|:---|
+| `?+` | non-empty | `()` | `t a` |
+| `?!` | exactly one element | `CardinalityError (t a)` | `a` |
+| `?∅` | empty | `t a` | `()` |
+
+```haskell
+data CardinalityError ta = IsEmpty | TooMany ta
+
+cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e   -- a fold for it
+```
+
+When an operator throws, the error mapping is applied to the failure info, the
+result is thrown, and the monadic sequence stops. `?~` and `??~` never throw:
+they put a default value on the happy path instead.
+
 ## Gotcha: an empty collection passes
 
-On a traversable, `?` and `??` check that **every** element succeeds
-(∀). Over an empty collection that holds vacuously, like `and []`:
+On a traversable, `?` and `??` check that **every** element succeeds (∀). Over
+an empty collection that holds vacuously, like `and []`:
 
 ```haskell
 pure ([] :: [Bool]) ? err403    -- succeeds, with []
 ```
 
-That surprises a permission check where "no rows" should mean "not
-permitted". Require at least one element first, with `?+` (∃), and
-chain the two checks:
+In a permission check, "no rows" should mean "not permitted", so require at least
+one element first, with `?+`:
 
 ```haskell
 _ <- runQuery (permitted uid asset) ? err500 ?+ err403 ? err403
---                                    ^ db error ^ >= 1 row ^ all True
+--                                  ^ db error ^ >= 1 row ^ all True
 ```
 
-The cardinality operators are the other quantifiers, so the usual
-conditions are each one operator, and they chain:
+Each quantifier has an operator, and they chain:
 
-| Operator                            | Succeeds when                       | Logic                           |
-|-------------------------------------|-------------------------------------|---------------------------------|
-| `?` / `??` on `[Bool]`, `[Maybe a]` | every element succeeds              | ∀ x ∈ xs. succeeds x            |
-| `?+`                                | there is at least one element       | ∃ x ∈ xs                        |
-| `?!`                                | there is exactly one element        | ∃! x ∈ xs                       |
-| `?∅`                                | there is no element                 | ¬∃ x ∈ xs                       |
-| `?+` then `?`                       | there is one, and every one succeeds | (∃ x ∈ xs) ∧ (∀ x ∈ xs. succeeds x) |
+| Operator | Succeeds when | Logic |
+|---|---|---|
+| `?` / `??` on `[Bool]`, `[Maybe a]` | every element succeeds | ∀ x ∈ xs. succeeds x |
+| `?+` | there is at least one element | ∃ x ∈ xs |
+| `?!` | there is exactly one element | ∃! x ∈ xs |
+| `?∅` | there is no element | ¬∃ x ∈ xs |
+| `?∃` | at least one element succeeds | ∃ x ∈ xs. succeeds x |
+| `?+` then `?` | there is one, and every one succeeds | (∃ x ∈ xs) ∧ (∀ x ∈ xs. succeeds x) |
 
-## More
-See [`railroad.md`](railroad.md) for a deeper tour, or just read the
-code at [`Railroad.hs`](src/Railroad.hs). It's a short file.
+## Behavior under an evolving code base
 
+The same operator works on `Maybe a`, `Either e a` and a list of either, so a
+dependency can change from `Maybe` to `Either` without touching its callers.
+[`examples/error-handling-styles`](examples/error-handling-styles) writes eleven
+handlers in three styles (railroad, plain `case`, and `orThrow`/`expectOne`
+helpers) and applies nine changes to each. Cells are edit sites / lines touched.
+
+| Change | railroad | helpers | `case` |
+|---|---|---|---|
+| `Maybe a` to `Either CacheError a` | 0 / 0 | 2 / 6 | 2 / 8 |
+| `Maybe a` to `[Maybe a]` (shards, first hit wins) | 3 / 5 | 3 / 6 | 3 / 5 |
+| `Either DbError [User]` to `Either DbError (Maybe User)` | 1 / 2 | 4 / 18 | 3 / 14 |
+| `Either DbError [Bool]` to `Either DbError Bool` (`SELECT EXISTS`) | 3 / 6 | 3 / 6 | 6 / 15 |
+| `validateName`: `Validation` to `Either` | 1 / 2 | 1 / 2 | 1 / 2 |
+| database timeout becomes 504 | 4 / 10 | 9 / 19 | 8 / 16 |
+| several grants per user: any granting row suffices | 3 / 6 | 4 / 7 | 3 / 12 |
+| audit-log every duplicate registration | 1 / 2 | 1 / 2 | 1 / 2 |
+| add a third fallback source | 1 / 1 | 1 / 6 | 2 / 11 |
+| **all nine** | **17 / 34** | **28 / 72** | **29 / 85** |
+
+Adding a source to a fallback chain is one line:
+
+```haskell
+-- before
+cache k ?| db k ? NotFound
+-- after
+cache k ?| db k ?| legacy k ? NotFound
+```
+
+In the `case` version it is another level of nesting around the existing branches.
+The timeout change took four edits, one per `? Unavailable`, because the error is a
+literal at each call site; with `?? dbErr` from the start it is one.
+
+One change went wrong silently. `validateName` changed from `Validation` to
+`Either`, and `pure (map validateName names) ?? BadRequest` kept compiling but
+reported the first problem instead of all of them. `?` over a collection chooses
+between stopping at the first failure and collecting every one by the element
+type. The other two styles stopped compiling. After changing a return type, check
+the operators at its call sites: a list where there was one value changes what `?`
+means, and an empty list passes (see the gotcha above).
+
+One LLM wrote the handlers. The test code base has eleven handlers
+with nine changes done to it and only GHC 9.10.3 was used.
+
+## Exercises
+
+[`examples/exercises`](examples/exercises) teaches the operators by translation.
+Each exercise is a function written with `case` and an empty stub. You fill in the
+stub with the operators until a test that runs both on random inputs agrees. There
+are 38 exercises in 13 lessons, with hints and an answer key.
+
+```
+cd examples/exercises && cabal test exercises
+```
+
+## Modules
+
+- `Railroad`: the operators for the `effectful` `Error` effect.
+- `Railroad.MonadError`: the same operators for any `MonadError`.
+- `Railroad.Bifurcate`: everything that does not throw. The `Bifurcate` class, the classifiers
+  (`neg`, `tagBy`, `successes`, `failures`, `anySucceeds`, `atLeastOne`, `exactlyOne`,
+  `noneAtAll`), `CardinalityError`, and the operators `?>`, `?~`, `??~`, `?|`, `?|<>`, `?<*`.
+
+The first two re-export it, so you rarely import it directly. The reasoning behind
+the design is in [`railroad.md`](railroad.md).
+
 ## Contact
-Feel free to DM me on https://x.com/mastratisi97 . I find it
-interesting to know if other people also have found this module
+
+DM me on https://x.com/mastratisi97 . I like to hear if other people find this
 useful.
diff --git a/src/Railroad.hs b/src/Railroad.hs
--- a/src/Railroad.hs
+++ b/src/Railroad.hs
@@ -7,16 +7,14 @@
 -- @Error@ effect, keeping the happy path clean. For @MonadError@ use "Railroad.MonadError".
 module Railroad
   ( module Railroad.Bifurcate
-  , module Railroad.Cardinality
   , collapse
-  , (??), (?), (?+), (?!), (?∅), (?@)
+  , (??), (?), (?+), (?!), (?∅), (?@), (?∃)
   ) where
 
-import           Data.Foldable           (toList)
+import           Data.Functor            ((<&>))
 import           Effectful               (Eff, type (:>))
 import           Effectful.Error.Dynamic (Error, throwError_)
 import           Railroad.Bifurcate
-import           Railroad.Cardinality
 
 
 -- | Unwraps the success case, or throws the failure mapped by the function.
@@ -36,35 +34,37 @@
 
 -- | Succeeds if non-empty, returning the collection; else throws the constant error.
 -- Chain it before @?@ on a traversable, which an empty collection passes.
+-- Sugar for @xs <&> 'atLeastOne' ? err@.
 (?+) :: forall es e t a. (Error e :> es, Foldable t)
      => Eff es (t a) -> e -> Eff es (t a)
-(?+) action err = do
-  xs <- action
-  if null xs then throwError_ err else pure xs
+action ?+ err = action <&> atLeastOne ? err
 
 
 -- | Succeeds if there is exactly one element, returning it; else throws the error built
--- from the 'CardinalityError'.
+-- from the 'CardinalityError'. Sugar for @xs <&> 'exactlyOne' ?? toErr@.
 (?!) :: forall es e t a. (Error e :> es, Foldable t)
      => Eff es (t a) -> (CardinalityError (t a) -> e) -> Eff es a
-(?!) action toErr = do
-  xs <- action
-  case toList xs of
-    []  -> throwError_ $ toErr IsEmpty
-    [x] -> pure x
-    _   -> throwError_ $ toErr $ TooMany xs
+action ?! toErr = action <&> exactlyOne ?? toErr
 
 
 -- | Succeeds if empty, returning @()@; else throws the error built from the collection.
+-- Sugar for @xs <&> 'noneAtAll' ?? toErr@.
 (?∅) :: forall es e t a. (Error e :> es, Foldable t)
      => Eff es (t a) -> ((t a) -> e) -> Eff es ()
-(?∅) action toErr = do
-  xs <- action
-  if null xs then pure () else throwError_ (toErr xs)
+action ?∅ toErr = action <&> noneAtAll ?? toErr
 
 -- | ASCII alias for '?∅'.
 (?@) :: forall es e t a. (Error e :> es, Foldable t)
      => Eff es (t a) -> ((t a) -> e) -> Eff es ()
 (?@) = (?∅)
 
-infixl 1 ??, ?, ?!, ?+, ?∅, ?@
+-- | Succeeds if at least one element succeeds, returning the successes; else throws the
+-- error built from every failure (none if the collection was empty). Unlike '?+' it asks
+-- whether an element succeeded, not whether there is one. On a nested base type such as
+-- @Maybe (Maybe a)@ the outer layer is the collection and the inner one is checked.
+-- Sugar for @xs <&> 'anySucceeds' ?? toErr@.
+(?∃) :: forall es e t a. (Error e :> es, Foldable t, Bifurcate a)
+     => Eff es (t a) -> ([CErr a] -> e) -> Eff es [CRes a]
+action ?∃ toErr = action <&> anySucceeds ?? toErr
+
+infixl 1 ??, ?, ?!, ?+, ?∅, ?@, ?∃
diff --git a/src/Railroad/Bifurcate.hs b/src/Railroad/Bifurcate.hs
--- a/src/Railroad/Bifurcate.hs
+++ b/src/Railroad/Bifurcate.hs
@@ -6,19 +6,37 @@
 
 -- |
 -- The 'Bifurcate' class, which splits a structure (@Bool@, @Maybe@, @Either@, @Validation@,
--- traversables of them) into a failure and a success case, and the operators that never
--- throw. The throwing operators are in "Railroad" and "Railroad.MonadError".
+-- traversables of them) into a failure and a success case, the classifiers built on it, and
+-- the operators that never throw. Nothing here depends on how a failure is thrown; the
+-- throwing operators are in "Railroad" (@effectful@) and "Railroad.MonadError" (@mtl@).
 module Railroad.Bifurcate
-  ( Bifurcate (..), CErr, CRes
+  ( -- * The class
+    Bifurcate (..), CErr, CRes
+    -- * Classifiers
+    -- $classifiers
+  , neg, tagBy, partitionBifurcate, successes, failures, anySucceeds
+    -- * Cardinality
+  , CardinalityError (..), cardinalityErr, atLeastOne, exactlyOne, noneAtAll
+    -- * Operators that never throw
+    -- $operators
   , (?>), (??~), (?~), (?|), (?|<>), (?<*)
   ) where
 
 import           Data.Bifunctor  (first)
 import           Data.Bool       (bool)
+import           Data.Either     (partitionEithers)
+import           Data.Foldable   (toList)
 import           Data.Kind       (Type)
 import           Data.Validation (Validation (..))
 
+-- $classifiers
+-- A classifier is a plain function into 'Either'. It needs no functor and never throws:
+-- apply it with @\<&\>@ and finish with any operator, e.g. @xs \<&\> 'exactlyOne' ?? f@.
 
+-- $operators
+-- These work in any 'Functor' or 'Monad' and never throw. Finish a chain with @?@ or @??@
+-- from "Railroad" or "Railroad.MonadError" to turn a remaining failure into a throw.
+
 -- | What the failure case carries: @()@ for 'Bool' and 'Maybe', @e@ for 'Either' and
 -- 'Validation', that of the element for a traversable.
 type family CErr f :: Type where
@@ -75,10 +93,74 @@
   bifurcate = bifurcate . sequenceA
 
 
+-- | Negation: swaps failure and success. A verb after it acts on a value that was meant to
+-- fail, and the error mapper sees what succeeded instead.
+--
+-- > lookupUser email <&> neg ?? const err409     -- Nothing passes, Just _ is a 409
+--
+-- On a traversable the failure is "some element failed", so @neg@ is the negation of the
+-- all-succeed reading: @xs <&> neg ? e@ passes exactly when @xs ? e@ would throw.
+-- @neg . atLeastOne@ is @noneAtAll@, and @neg (neg x)@ is @'bifurcate' x@.
+neg :: Bifurcate a => a -> Either (CRes a) (CErr a)
+neg = either Right Left . bifurcate
+
+-- | The classifier behind @(?>)@: @Right@ if the value satisfies the predicate, else @Left@
+-- carrying it.
+tagBy :: (a -> Bool) -> a -> Either a a
+tagBy predicate x = if predicate x then Right x else Left x
+
+-- | Splits a collection of results into its failures and its successes, in order.
+partitionBifurcate :: (Foldable t, Bifurcate a) => t a -> ([CErr a], [CRes a])
+partitionBifurcate = partitionEithers . map bifurcate . toList
+
+-- | The success values of a collection of results; the failures are dropped.
+successes :: (Foldable t, Bifurcate a) => t a -> [CRes a]
+successes = snd . partitionBifurcate
+
+-- | The failure values of a collection of results.
+failures :: (Foldable t, Bifurcate a) => t a -> [CErr a]
+failures = fst . partitionBifurcate
+
+-- | Classifies a collection of results as having at least one success. The success carries
+-- every success value; the failure carries every failure value (none if the collection
+-- was empty): @xs <&> anySucceeds ?? f@.
+anySucceeds :: (Foldable t, Bifurcate a) => t a -> Either [CErr a] [CRes a]
+anySucceeds xs = case partitionBifurcate xs of
+  (es, [])  -> Left es
+  (_, rs)   -> Right rs
+
+-- | Why a collection was not a single element: empty, or too many (carrying it).
+data CardinalityError ta = IsEmpty | TooMany ta
+  deriving (Eq, Show)
+
+-- | Fold for 'CardinalityError': one result per case.
+cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e
+cardinalityErr onEmpty onTooMany = \case
+  IsEmpty -> onEmpty
+  TooMany xs -> onTooMany xs
+
+-- | Classifies a collection as non-empty. The failure carries nothing, so the result is
+-- @Bifurcate@ with @()@ as the error info: @xs <&> atLeastOne ? e@ is @xs ?+ e@.
+atLeastOne :: Foldable t => t a -> Maybe (t a)
+atLeastOne xs = if null xs then Nothing else Just xs
+
+-- | Classifies a collection as having exactly one element, returning it. The failure says
+-- whether there were none or too many: @xs <&> exactlyOne ?? f@ is @xs ?! f@.
+exactlyOne :: Foldable t => t a -> Either (CardinalityError (t a)) a
+exactlyOne xs = case toList xs of
+  []  -> Left IsEmpty
+  [x] -> Right x
+  _   -> Left (TooMany xs)
+
+-- | Classifies a collection as empty. The failure carries the collection:
+-- @xs <&> noneAtAll ?? f@ is @xs ?∅ f@.
+noneAtAll :: Foldable t => t a -> Either (t a) ()
+noneAtAll xs = if null xs then Right () else Left xs
+
 -- | Tags the value: @Right@ if it satisfies the predicate, else @Left@ carrying it. Finish
 -- with @?@ or @??@ (the mapper sees the rejected value): @m ?> p ? err@.
 (?>) :: forall m a. Functor m => m a -> (a -> Bool) -> m (Either a a)
-action ?> predicate = (\x -> if predicate x then Right x else Left x) <$> action
+action ?> predicate = tagBy predicate <$> action
 
 -- | Unwraps the success case, or recovers with a value computed from the error info.
 (??~) :: forall m a. (Functor m, Bifurcate a) => m a -> (CErr a -> CRes a) -> m (CRes a)
diff --git a/src/Railroad/Cardinality.hs b/src/Railroad/Cardinality.hs
deleted file mode 100644
--- a/src/Railroad/Cardinality.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-{-# LANGUAGE LambdaCase #-}
-
--- |
--- The error info of the cardinality operators (@?!@ in "Railroad" and "Railroad.MonadError"):
--- why a collection was not a single element.
-module Railroad.Cardinality
-  ( CardinalityError (..), cardinalityErr
-  ) where
-
--- | Why a collection was not a single element: empty, or too many (carrying it).
-data CardinalityError ta = IsEmpty | TooMany ta
-
--- | Fold for 'CardinalityError': one result per case.
-cardinalityErr :: e -> (ta -> e) -> CardinalityError ta -> e
-cardinalityErr onEmpty onTooMany = \case
-  IsEmpty -> onEmpty
-  TooMany xs -> onTooMany xs
diff --git a/src/Railroad/MonadError.hs b/src/Railroad/MonadError.hs
--- a/src/Railroad/MonadError.hs
+++ b/src/Railroad/MonadError.hs
@@ -6,15 +6,13 @@
 
 module Railroad.MonadError
   ( module Railroad.Bifurcate
-  , module Railroad.Cardinality
   , collapse
-  , (??), (?), (?+), (?!), (?∅), (?@)
+  , (??), (?), (?+), (?!), (?∅), (?@), (?∃)
   ) where
 
 import           Control.Monad.Except (MonadError (..))
-import           Data.Foldable        (toList)
+import           Data.Functor         ((<&>))
 import           Railroad.Bifurcate
-import           Railroad.Cardinality
 
 -- | Unwraps the success case, or throws the failure mapped by the function.
 collapse :: (MonadError e m, Bifurcate f)
@@ -34,33 +32,35 @@
 
 -- | Succeeds if non-empty, returning the collection; else throws the constant error.
 -- Chain it before @?@ on a traversable, which an empty collection passes.
+-- Sugar for @xs <&> 'atLeastOne' ? err@.
 (?+) :: forall m e t a. (MonadError e m, Foldable t)
      => m (t a) -> e -> m (t a)
-(?+) action err = do
-  xs <- action
-  if null xs then throwError err else pure xs
+action ?+ err = action <&> atLeastOne ? err
 
 -- | Succeeds if there is exactly one element, returning it; else throws the error built
--- from the 'CardinalityError'.
+-- from the 'CardinalityError'. Sugar for @xs <&> 'exactlyOne' ?? toErr@.
 (?!) :: forall m e t a. (MonadError e m, Foldable t)
      => m (t a) -> (CardinalityError (t a) -> e) -> m a
-(?!) action toErr = do
-  xs <- action
-  case toList xs of
-    []  -> throwError $ toErr IsEmpty
-    [x] -> pure x
-    _   -> throwError $ toErr $ TooMany xs
+action ?! toErr = action <&> exactlyOne ?? toErr
 
 -- | Succeeds if empty, returning @()@; else throws the error built from the collection.
+-- Sugar for @xs <&> 'noneAtAll' ?? toErr@.
 (?∅) :: forall m e t a. (MonadError e m, Foldable t)
      => m (t a) -> (t a -> e) -> m ()
-(?∅) action toErr = do
-  xs <- action
-  if null xs then pure () else throwError (toErr xs)
+action ?∅ toErr = action <&> noneAtAll ?? toErr
 
 -- | ASCII alias for '?∅'.
 (?@) :: forall m e t a. (MonadError e m, Foldable t)
      => m (t a) -> (t a -> e) -> m ()
 (?@) = (?∅)
 
-infixl 1 ??, ?, ?!, ?+, ?∅, ?@
+-- | Succeeds if at least one element succeeds, returning the successes; else throws the
+-- error built from every failure (none if the collection was empty). Unlike '?+' it asks
+-- whether an element succeeded, not whether there is one. On a nested base type such as
+-- @Maybe (Maybe a)@ the outer layer is the collection and the inner one is checked.
+-- Sugar for @xs <&> 'anySucceeds' ?? toErr@.
+(?∃) :: forall m e t a. (MonadError e m, Foldable t, Bifurcate a)
+     => m (t a) -> ([CErr a] -> e) -> m [CRes a]
+action ?∃ toErr = action <&> anySucceeds ?? toErr
+
+infixl 1 ??, ?, ?!, ?+, ?∅, ?@, ?∃
diff --git a/test/Railroad/BifurcateSpec.hs b/test/Railroad/BifurcateSpec.hs
--- a/test/Railroad/BifurcateSpec.hs
+++ b/test/Railroad/BifurcateSpec.hs
@@ -4,7 +4,7 @@
 
 import           Control.Monad.State    (modify, runState)
 import           Data.Bifunctor         (first)
-import           Data.Either            (isLeft)
+import           Data.Either            (isLeft, isRight)
 import           Data.Functor.Identity
 import           Data.Maybe             (listToMaybe)
 import           Data.Proxy             (Proxy (..))
@@ -81,3 +81,92 @@
     it "[Either], [Either]" $ law (Proxy @[Either [Int] Int]) (Proxy @[Either [Int] Int])
     it "is associative" $ property $ \(a :: Either [Int] Int) (b :: Either [Int] Int) (c :: Either [Int] Int) ->
       runIdentity ((pure a ?|<> pure b) ?|<> pure c) === runIdentity (pure a ?|<> (pure b ?|<> pure c))
+
+  describe "tagBy" $ do
+    it "keeps the value on both sides" $ property $ \(p :: Fun Int Bool) (x :: Int) ->
+      either id id (tagBy (applyFun p) x) === x
+    it "is Right exactly when the predicate holds" $ property $ \(p :: Fun Int Bool) (x :: Int) ->
+      isRight (tagBy (applyFun p) x) === applyFun p x
+
+  describe "partitionBifurcate, successes, failures" $ do
+    let laws :: forall a. (Shape a, Bifurcate [a], CErr [a] ~ CErr a, CRes [a] ~ [CRes a])
+             => Proxy a -> Property
+        laws _ = property $ \(xs :: [a]) (ys :: [a]) ->
+          let (es, rs) = partitionBifurcate xs
+          in (length es + length rs === length xs)
+             .&&. (partitionBifurcate (xs ++ ys) === (es <> failures ys, rs <> successes ys))
+             .&&. (isRight (bifurcate xs) === null es)
+             .&&. (either (const True) (== rs) (bifurcate xs))
+             .&&. (successes xs === rs)
+             .&&. (failures xs === es)
+        single :: forall a. Shape a => Proxy a -> Property
+        single _ = property $ \(x :: a) ->
+          (successes [x] === either (const []) pure (bifurcate x))
+          .&&. (failures [x] === either pure (const []) (bifurcate x))
+    it "Bool"       $ laws (Proxy @Bool)
+    it "Maybe"      $ laws (Proxy @(Maybe Int))
+    it "Either"     $ laws (Proxy @(Either Int Int))
+    it "Validation" $ laws (Proxy @(Validation [Int] Int))
+    forShapes "a singleton splits as its element does" single
+
+  describe "anySucceeds" $ do
+    let law :: forall a. (Shape a) => Proxy a -> Property
+        law _ = property $ \(xs :: [a]) ->
+          let r = anySucceeds xs
+          in (isRight r === any (isRight . bifurcate) xs)
+             .&&. (r === (if any (isRight . bifurcate) xs then Right (successes xs) else Left (failures xs)))
+             .&&. (either length (const 0) r === (if any (isRight . bifurcate) xs then 0 else length xs))
+    it "Bool"       $ law (Proxy @Bool)
+    it "Maybe"      $ law (Proxy @(Maybe Int))
+    it "Either"     $ law (Proxy @(Either Int Int))
+    it "Validation" $ law (Proxy @(Validation [Int] Int))
+    it "fails on an empty collection with no failures" $
+      anySucceeds ([] :: [Maybe Int]) `shouldBe` Left []
+
+  describe "neg" $ do
+    it "Bool and Maybe swap to the unit failure" $ do
+      neg True `shouldBe` Left ()
+      neg False `shouldBe` Right ()
+      neg (Just 'x') `shouldBe` Left 'x'
+      neg (Nothing :: Maybe Char) `shouldBe` Right ()
+    forShapes "swaps failure and success" $ \(_ :: Proxy a) -> property $
+      \(x :: a) -> neg x === either Right Left (bifurcate x)
+    forShapes "is an involution: neg (neg x) = bifurcate x" $ \(_ :: Proxy a) -> property $
+      \(x :: a) -> neg (neg x) === bifurcate x
+    it "of a traversable passes exactly when some element failed" $ property $
+      \(xs :: [Either Int Int]) -> isRight (neg xs) === any (isLeft . bifurcate) xs
+    it "carries the first failure of a traversable" $ property $ \(xs :: [Either Int Int]) ->
+      neg xs === (case [e | Left e <- xs] of { e : _ -> Right e; [] -> Left [a | Right a <- xs] })
+    it "of atLeastOne is noneAtAll" $ property $ \(xs :: [Int]) ->
+      neg (atLeastOne xs) === noneAtAll xs
+    it "turns anySucceeds into all-failed" $ property $ \(xs :: [Either Int Int]) ->
+      isRight (neg (anySucceeds xs)) === all (isLeft . bifurcate) xs
+
+  describe "atLeastOne" $ do
+    it "classifies a list by emptiness, keeping the list" $ property $ \(xs :: [Int]) ->
+      atLeastOne xs === (if null xs then Nothing else Just xs)
+    it "works on any Foldable" $ do
+      atLeastOne (Just 'a') `shouldBe` Just (Just 'a')
+      atLeastOne (Nothing :: Maybe Char) `shouldBe` Nothing
+
+  describe "exactlyOne" $ do
+    it "returns the element of a singleton, else says why not" $ property $ \(xs :: [Int]) ->
+      exactlyOne xs === (case xs of { [] -> Left IsEmpty; [x] -> Right x; _ -> Left (TooMany xs) })
+    it "works on any Foldable" $ do
+      exactlyOne (Just 'a') `shouldBe` Right 'a'
+      exactlyOne (Nothing :: Maybe Char) `shouldBe` Left IsEmpty
+
+  describe "noneAtAll" $ do
+    it "succeeds on an empty collection, else returns it as the failure" $ property $ \(xs :: [Int]) ->
+      noneAtAll xs === (if null xs then Right () else Left xs)
+
+  describe "the three agree with each other" $ do
+    it "exactlyOne reports IsEmpty exactly when noneAtAll succeeds" $ property $ \(xs :: [Int]) ->
+      (exactlyOne xs == Left IsEmpty) === (noneAtAll xs == Right ())
+    it "a list of one element satisfies atLeastOne and exactlyOne but not noneAtAll" $ property $ \(x :: Int) ->
+      (atLeastOne [x], exactlyOne [x], noneAtAll [x]) === (Just [x], Right x, Left [x])
+
+  describe "CardinalityError" $ do
+    it "cardinalityErr is the eliminator" $ property $ \(n :: Int) (xs :: [Int]) ->
+      (cardinalityErr n length (IsEmpty :: CardinalityError [Int]) === n)
+      .&&. (cardinalityErr n length (TooMany xs) === length xs)
diff --git a/test/Railroad/MonadErrorSpec.hs b/test/Railroad/MonadErrorSpec.hs
--- a/test/Railroad/MonadErrorSpec.hs
+++ b/test/Railroad/MonadErrorSpec.hs
@@ -6,6 +6,7 @@
 import           Control.Monad.Except
 import           Control.Monad.State   (modify, runState)
 import           Data.Bifunctor        (first)
+import           Data.Either           (isRight)
 import           Data.Functor          ((<&>))
 import           Data.Functor.Identity
 import           Data.Proxy            (Proxy (..))
@@ -61,6 +62,21 @@
       run (pure (Right 4 :: Either Int Int) ?<* (\e -> modify (e :)) ? "failed") `shouldBe` (Right 4, [])
     it "gives the rejected value of ?> to the error mapper" $ do
       runMonadError (pure (4 :: Int) ?> (> 5) ?? \n -> "rejected " ++ show n) `shouldBe` Left "rejected 4"
+
+  describe "?∃ (at least one success)" $ do
+    it "returns the successes, or throws every failure" $ property $ \(xs :: [Either Int Int]) ->
+      runMonadError (pure xs ?∃ show)
+        === (if any isRight xs then Right [a | Right a <- xs] else Left (show [e | Left e <- xs]))
+    it "throws on an empty collection, with no failures" $
+      runMonadError (pure ([] :: [Maybe Int]) ?∃ show) `shouldBe` Left "[]"
+    it "passes the successes of a Maybe collection and throws unit failures" $ do
+      runMonadError (pure [Just 1, Nothing, Just (3 :: Int)] ?∃ show) `shouldBe` Right [1, 3]
+      runMonadError (pure [Nothing, Nothing :: Maybe Int] ?∃ show) `shouldBe` Left "[(),()]"
+    it "throws where ?+ passes: every element failed" $ do
+      runMonadError (pure [Left 1, Left 2 :: Either Int Int] ?+ "none") `shouldSatisfy` isRight
+      runMonadError (pure [Left 1, Left 2 :: Either Int Int] ?∃ show) `shouldBe` Left "[1,2]"
+    it "chains left to right with the other operators" $ do
+      runMonadError (pure (Right [Just 1, Nothing] :: Either String [Maybe Int]) ? "db" ?∃ show) `shouldBe` Right [1]
 
   describe "README" $ do
     it "runs the first example" $ do
diff --git a/test/RailroadSpec.hs b/test/RailroadSpec.hs
--- a/test/RailroadSpec.hs
+++ b/test/RailroadSpec.hs
@@ -3,6 +3,7 @@
 module RailroadSpec where
 
 import           Data.Bifunctor          (first)
+import           Data.Either             (isRight)
 import           Data.Functor            ((<&>))
 import           Data.Proxy              (Proxy (..))
 import           Effectful
@@ -61,3 +62,18 @@
       run (pure (Right 4 :: Either Int Int) ?<* (\e -> modify (e :)) ? "failed") `shouldBe` (Right 4, [])
     it "gives the rejected value of ?> to the error mapper" $ do
       runRail (pure (4 :: Int) ?> (> 5) ?? \n -> "rejected " ++ show n) `shouldBe` Left "rejected 4"
+
+  describe "?∃ (at least one success)" $ do
+    it "returns the successes, or throws every failure" $ property $ \(xs :: [Either Int Int]) ->
+      runRail (pure xs ?∃ show)
+        === (if any isRight xs then Right [a | Right a <- xs] else Left (show [e | Left e <- xs]))
+    it "throws on an empty collection, with no failures" $
+      runRail (pure ([] :: [Maybe Int]) ?∃ show) `shouldBe` Left "[]"
+    it "passes the successes of a Maybe collection and throws unit failures" $ do
+      runRail (pure [Just 1, Nothing, Just (3 :: Int)] ?∃ show) `shouldBe` Right [1, 3]
+      runRail (pure [Nothing, Nothing :: Maybe Int] ?∃ show) `shouldBe` Left "[(),()]"
+    it "throws where ?+ passes: every element failed" $ do
+      runRail (pure [Left 1, Left 2 :: Either Int Int] ?+ "none") `shouldSatisfy` isRight
+      runRail (pure [Left 1, Left 2 :: Either Int Int] ?∃ show) `shouldBe` Left "[1,2]"
+    it "chains left to right with the other operators" $ do
+      runRail (pure (Right [Just 1, Nothing] :: Either String [Maybe Int]) ? "db" ?∃ show) `shouldBe` Right [1]
