Simple INI file parser in Haskell I've written a rudimentary parser for INI files:

{-# LANGUAGE OverloadedStrings   #-}

import qualified Data.Map           as M
import           Data.Maybe         (fromMaybe)
import qualified Data.Text          as T

type Ini = M.Map T.Text Section

data Section =
  Section
    { name       :: T.Text
    , properties :: M.Map T.Text T.Text
    }
  deriving (Show)

main :: IO ()
main = parseIni iniFilePath &gt;&gt;= \ini -&gt; putStrLn $ "Parsed INI: " ++ show ini
  where
    iniFilePath = "/home/me/test.ini"

parseIni :: FilePath -&gt; IO Ini
parseIni iniFilePath = parseToIni . T.pack &lt;$&gt; readFile iniFilePath

parseToIni :: T.Text -&gt; Ini
parseToIni stringToParse =
  -- We return the parsed Ini, not the helper values
  firstOfTriple $
  foldr
    (\line (ini, currentSectionMaybe, lineIndex) -&gt;
        -- We're at a new section start or the end of the file → add the previous section
        -- to the parsed Ini value and create a new section
      if isSectionHeader line || lineIndex &gt;= length lines - 1
        then let updatedIni = addSection ini currentSectionMaybe
              in (updatedIni, Just $ Section (getSectionName line) M.empty, 1 + lineIndex)
        else (ini, updateSection currentSectionMaybe line, 1 + lineIndex))
    (M.empty, Nothing, 0) $
  -- Since foldr is right associative we would process the lines starting with the last one, that's
  -- why we reverse the list of lines
  reverse lines
  where
    lines :: [T.Text]
    lines = T.splitOn "\n" stringToParse

firstOfTriple :: (a, b, c) -&gt; a
firstOfTriple (x, _, _) = x

parseProperty :: T.Text -&gt; Maybe (T.Text, T.Text)
parseProperty line =
  case T.splitOn "=" line of
    [name, value] -&gt; Just (T.strip name, T.strip value)
    _             -&gt; Nothing

updateSection :: Maybe Section -&gt; T.Text -&gt; Maybe Section
updateSection sectionMaybe line = fmap updateSection' sectionMaybe
  where
    updateSection' :: Section -&gt; Section
    updateSection' section =
      -- Add the property to the section if the property can be parsed.
      -- Otherwise, leave the section as it were
      maybe
        section
        (\(propName, value) -&gt; Section (name section) (M.insert propName value (properties section)))
        (parseProperty line)

getSectionName :: T.Text -&gt; T.Text
getSectionName line = fromMaybe line headerWithoutBracketsMaybe
  where
    headerWithoutBracketsMaybe = T.stripPrefix "[" line &gt;&gt;= T.stripSuffix "]"

isSectionHeader :: T.Text -&gt; Bool
isSectionHeader line = T.isPrefixOf "[" strippedLine &amp;&amp; T.isSuffixOf "]" strippedLine
  where
    strippedLine = T.strip line

addSection :: Ini -&gt; Maybe Section -&gt; Ini
addSection ini sectionMaybe = maybe ini (\section -&gt; M.insert (name section) section ini) sectionMaybe


I'd love to get feedback on how to simplify the code and/or make it more readable.

Things I'm aware of and ok with at the moment:


The parser doesn't support comments
addSection could be eta-reduced
I don't use a parsing library like Parsec
I don't use lenses
I know you said you were fine with not using a parser combinator library like parsec, but I thought you might like to see the how the same thing might look using one, so I wrote an Attoparsec based parser for your data types:

{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module IniParser where

import           Control.Monad               ( void )
import           Data.Attoparsec.Text        ( Parser
                                             , char
                                             , endOfInput
                                             , endOfLine
                                             , many'
                                             , many1
                                             , notInClass
                                             , parseOnly
                                             , satisfy
                                             , space
                                             )
import           Data.Map.Strict             ( Map )
import qualified Data.Map.Strict      as Map
import           Data.Text                   ( Text
                                             , pack
                                             )
import           System.Environment          ( getArgs )

type Ini = Map Text Section

data Section = Section
  { name       :: Text
  , properties :: Map Text Text
  } deriving (Show)

main :: IO ()
main = do
  [path] &lt;- getArgs
  parseIniFile path &gt;&gt;= \case
    Right ini -&gt; putStrLn $ "Parsed INI: " ++ show ini
    Left err  -&gt; putStrLn $ "ERROR parsing ini: " ++ err

parseIniFile :: FilePath -&gt; IO (Either String Ini)
parseIniFile iniFilePath = parseIni . pack &lt;$&gt; readFile iniFilePath

parseIni :: Text -&gt; Either String Ini
parseIni = parseOnly ini

ini :: Parser Ini
ini = do
  defaultSection &lt;- lexeme (Section "" &lt;$&gt; (Map.fromList &lt;$&gt; many' property))
  namedSections  &lt;- lexeme (many' section)
  void $ endOfInput
  let allSections | null (properties defaultSection) = namedSections
                  | otherwise = defaultSection:namedSections
  pure . Map.fromList . map (\section -&gt; (name section, section))
    $ allSections

section :: Parser Section
section = Section &lt;$&gt; sectionName &lt;*&gt; (Map.fromList &lt;$&gt; many' (lexeme property))

sectionName :: Parser Text
sectionName = char '[' *&gt; sectionNameChars &lt;* char ']' &lt;* endOfLine

sectionNameChars :: Parser Text
sectionNameChars = pack &lt;$&gt; many' (satisfy $ notInClass "]\r\n")

property :: Parser (Text, Text)
property = (,) &lt;$&gt; propertyName &lt;*&gt; (lexeme (char '=') *&gt; propertyValue)

propertyName :: Parser Text
propertyName = pack &lt;$&gt; many' (satisfy $ notInClass "=\r\n\t ")

propertyValue :: Parser Text
propertyValue = pack &lt;$&gt; many' (satisfy $ notInClass "\r\n")

lexeme :: Parser a -&gt; Parser a
lexeme p = whitespace *&gt; p &lt;* whitespace

whitespace :: Parser String
whitespace = many' space


I think the main strength of the approach is self pretty self-evident.  It eliminates all the multi-line lambdas, the entire foldr, etc. which (at least IMHO) really obscures the essence of what the code is expressing.

Additionally I've restricted the use of qualified imports to a single one usage where I think it makes the code more readable, though your taste may vary.

You can see the whole stack based project here if you're interested.