Haskell program with basic CLI parsing, file reading, and decoding file contents into specified types
First things first: great work on adding a type signature on every function. Now, let's see how we can improve the code.

Use interesting case first, others later
When we pattern match in a binding or in a case expression, it's often easier to start with the interesting case first and then handle the Error cases:
decodeTeacher :: PersonDecoder
decodeTeacher encoded =
  case lines of (name:dept:_) -&gt; Just (Teacher name dept)
                _             -&gt; Nothing
  where lines = splitOn &quot;\n&quot; encoded

decodeStudent :: PersonDecoder
decodeStudent encoded =
  case lines of (name:age:_) -&gt; decodeStudentLines name age
                _            -&gt; Nothing
  where lines = splitOn &quot;\n&quot; encoded


Check the standard library for already existing functionality
We stay at those functions. The name lines is very unfortunate, as there is already a lines in the Prelude. It's a function with the following signature:
lines :: String -&gt; [String]

With that function, we can get rid of splitOn:
decodeTeacher :: PersonDecoder
decodeTeacher encoded =
  case lines encoded of
    (name:dept:_) -&gt; Just (Teacher name dept)
    _             -&gt; Nothing

decodeStudent :: PersonDecoder
decodeStudent encoded =
  case lines encoded of
    (name:age:_) -&gt; decodeStudentLines name age
    _            -&gt; Nothing


Prefer simpler logic where possible
Next, we have a look at parseArgsSuffix:
parseArgsSuffix :: CLIParser
parseArgsSuffix [] = Nothing
parseArgsSuffix (fname:_) =
  if hasSuffix fname &quot;student&quot; then Just (decodeStudent, openFile fname ReadMode)
  else if hasSuffix fname &quot;teacher&quot; then Just (decodeTeacher, openFile fname ReadMode)
  else Nothing

That's a rather long if expression. A guard may be easier to read and handle:
parseArgsSuffix :: CLIParser
parseArgsSuffix [] = Nothing
parseArgsSuffix (fname:_)
  | fname `hasSuffix` &quot;student&quot; = Just (decodeStudent, openFile fname ReadMode)
  | fname `hasSuffix` &quot;teacher&quot; = Just (decodeTeacher, openFile fname ReadMode)
  | otherwise                   = Nothing

Note that we should keep the uninteresting case here first, as it improves readability. However, we're repeating ourselves. Something along
parseArgsSuffix :: CLIParser
parseArgsSuffix [] = Nothing
parseArgsSuffix (fname:_)
  | fname `hasSuffix` &quot;student&quot; = decodeVia decodeStudent 
  | fname `hasSuffix` &quot;teacher&quot; = decodeVia decodeTeacher
  | otherwise                   = Nothing
  where
    decodeVia f = Just (f, openFile fname ReadMode)

might reduce the duplication. Now, let's have a look at hasSuffix:
hasSuffix :: String -&gt; String -&gt; Bool
hasSuffix filename suffix = if length components == 0 || length components == 1
  then False
  else last components == suffix
  where
    components = splitOn &quot;.&quot; filename

Again, splitOn is an overkill here. We only want to know if our filename ends with the given extension. The function isSuffixOf from Data.List handles that fine:
hasSuffix :: String -&gt; String -&gt; Bool
hasSuffix filename suffix = ('.' : suffix) `isSuffixOf` filename

We could probably even remove hasSuffix completely that way.

Make sure that functions are total
parseArgsFlag is partial. If we use parseArgsFlags on [&quot;whoops!&quot;, &quot;-t&quot;], then none of the patterns match:
parseArgsFlag :: CLIParser
parseArgsFlag [] = Nothing
parseArgsFlag (&quot;-t&quot;:_) = Just (decodeTeacher, wrapStdIn)
parseArgsFlag (&quot;-s&quot;:_) = Just (decodeStudent, wrapStdIn)
parseArgsFlag (&quot;--teacher&quot;:_) = parseArgsFlag [&quot;-t&quot;]
parseArgsFlag (&quot;--student&quot;:_) = parseArgsFlag [&quot;-s&quot;]

If that is intended, then everything is fine. However, if you want to handle those cases, then add a match all pattern:
parseArgsFlag _               = Nothing

I would prefer something like this, by the way:
parseArgsFlag :: CLIParser
parseArgsFlag args
  | any (`elem` args) [&quot;-t&quot;, &quot;--teacher&quot;] = Just (decodeTeacher, wrapStdIn)
  | any (`elem` args) [&quot;-s&quot;, &quot;--student&quot;] = Just (decodeStudent, wrapStdIn)
  | otherwise                             = Nothing