`read` re-implemented for Int's
The change I'd make, respecting your overall design, is to refactor chrToNum as this:

chrToNum :: [(Char, Int)]
chrToNum = zip "0123456789" [0..]


Apart from that, if we are willing to slightly alter the design, I would separate the string validation from the computation of the Int represented. The string validation can be done using mapM :: (a -&gt; m b) -&gt; [a] -&gt; m [b] (this way we avoid explicitly writing the recursion).

validateString :: String -&gt; Maybe [Int]
validateString = mapM readChr


Then, you can define a function computing the Int represented by a list of digits (I use foldl' which requires you to add import Data.List but you can equally well use reverse + foldr as you originally did)

represents :: [Int] -&gt; Int
represents = foldl' (\ ih z -&gt; 10 * ih + z) 0


Finally, putting it all together we get:

readNum :: String -&gt; Maybe Int
readNum = fmap represents . validateString