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:
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 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 =
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.