Prolog in Haskell - Compile-time parsing and conclusion

In the previous log, our Prolog implementation we have everything, from parsing to evaluation, we will parse at compile-time and conclude.

Few years ago, we have seen, few years ago, how to lift parsing at compile-time.

Let's reuse it, as follows:

import Language.Haskell.TH
import Language.Haskell.TH.Quote

qqProlog :: QuasiQuoter
qqProlog =
  QuasiQuoter
    { quoteExp = \str -> do
        loc <- location
        void $ runIO $ parseIO (first (T.pack . show) . parseProlog . T.unpack) loc str
        let unwrap =
              AppE
                ( AppE
                    (VarE 'either)
                    (AppE (VarE 'const) (AppE (VarE 'error) (LitE (StringL "run-time error"))))
                )
                ( AppE
                    (VarE 'mappend)
                    (ListE [VarE 'failP, VarE 'cut, VarE 'negateP])
                )
            parse =
              AppE
                (VarE 'parseProlog)
                (LitE (StringL str))

        pure $ AppE unwrap parse,
      quotePat = undefined,
      quoteType = undefined,
      quoteDec = undefined
    }
  where
    parseIO :: (Text -> Either Text a) -> Loc -> String -> IO a
    parseIO p loc str =
      case p $ T.pack str of
        Left err ->
          throwIO $
            userError $
              mconcat
                [ "'",
                  str,
                  "'",
                  " is not a valid Prolog program ",
                  "(",
                  T.unpack err,
                  ")",
                  " at ",
                  loc_filename loc,
                  ":",
                  show (loc_start loc),
                  "-",
                  show (loc_start loc)
                ]
        Right a ->
          return a

There are two ways to handle it:

  • Rework the parser to emit GHC data-types, turn our types to be liftable
  • Call parsing at run-time, making the quasi quotation minimal

The other thing to consider is to add the standard predicates: !/0, fail/0, \+/1.

It's usable as any quasi quotes, as follows:

fruitsPredicates :: [Predicate]
fruitsPredicates =
  [qqProlog|
fruit(melon).
salad(melon).
fruitSalad(X) :- fruit(X), salad(X).
 |]

Time to wrap up!

Our final implementation can be broke down as:

  • Core: 1 type class, 10 types, 14 functions, 72 LoC
  • Standard predicates: 4, 18 LoC
  • Support: 2 types, 2 functions, 18 LoC
  • Parsing: 1 data-type, 4 parsers, 3 functions, 31 LoC

The code implementation has barely half of the LoC, which does not surprise me.

A bit than 50 years ago was published Algorithms + Data Structures Programs by Niklaus Wirth.

Logic and Stream are simple by themselves, but they are the cornerstone of the implementation.

Occasionally, I speak with software developers strongly arguing that functional programming is not a good fit for real-world domain modeling because we only create anemic types.

The thing is, most of the classes I see in real-world OOP programs, are large, highly coupled and have a low cohesion, while FP data-types usually are small, with a clearly defined purpose.

I was planning to dive into Prolog for many years, it was pleasant the iterate over each feature, refactoring after refactoring.