/polyglot

Haskell to Purescript & Scala 3 transpiler

Primary LanguageHaskellGNU General Public License v3.0GPL-3.0

polyglot

Description

CLI tool to transpile Haskell modules to several target languages.

The CLI can convert individual Haskell files as well as recursively convert directory trees (or entire projects).

There are a few options available to, for example, watch a file / directory and reactively convert it whenever modified, as well as to format the output target language files.

At the moment, only parsing of Haskell 98 / 2010 standards along with a subset of GHC Syntax Extensions is supported (for example, there is currently no support for either Template Haskell or some of the GHC Syntax Extensions, such as GADTs and Type Families).

Usage

Usage: polyglot (-l|--language ARG) (-i|--input ARG) (-o|--output ARG) 
                [-f|--format] [-w|--watch] [--clear]

  Compile Haskell file(s) into a target language.

Available options:
  -h,--help                Show this help text
  -l,--language ARG        Target language
  -i,--input ARG           Path of input Haskell file or directory
  -o,--output ARG          Path of output file or directory
  -f,--format              Apply formatter on output file(s)
  -w,--watch               Watch for changes and convert automatically
  --clear                  Clear the output directory contents before conversion

Supported languages: Purescript, Scala

Details

This CLI tool aims to perform a one-to-one mapping between Haskell and target language constructs. This can be done in most cases, because all of the available target languages support many of Haskell key features that are not necessarily available in other mainstream languages, such as Higher Kinded Types, Typeclasses, GADTs & Higher Rank Polymorphism.

However, the conversion can be lossy, so some information can be lost in the process. At the same time, it can be necessary to provide some extra information in the target language version of the source file (most prominently (type / kind) signatures, due to less powerful type inference mechanisms than Hindley-Millner in the target language).

The resulting files will have a dependency on some kind of prelude library that will expose all of the usual functions, data types, type classes and instances included in the Haskell prelude.

Also, bear in mind that in some cases due to different call semantics (lazy or call-by-need vs strict) and also runtime support for features (such as tail call optimization), the resulting files in the target language will probably need on some cases to be manually adapted post conversion, to preserve or approximate to the original Haskell code runtime characteristics.

In any case, it can be helpful to check the output files and manually adapt them as desired, because many Haskell idioms may not be the best match in the target language (This can be specially the case for languages that are not in the ML family (such as Scala)).

Examples

Sample Haskell snippet:

data Language
  = Purescript
  | Scala
  deriving (Bounded, Enum, Eq, Ord, Show)

parserOption :: Bookhound.Parser a -> Options.Applicative.Mod Options.Applicative.OptionFields a -> Parser a
parserOption parser = option $ eitherReader $ reader
  where
    reader = mapLeft show . Bookhound.runParser parser . pack

Converted Purescript output (after formatting):

data Language
  = Purescript
  | Scala
derive instance Bounded Language
derive instance Enum Language
derive instance Eq Language
derive instance Ord Language
derive instance Show Language

parserOption :: forall a. Bookhound.Parser a -> Options.Applicative.Mod Options.Applicative.OptionFields a -> Parser a
parserOption parser = option $ eitherReader $ reader
  where
    reader = mapLeft show <<< Bookhound.runParser parser <<< pack

Sample Haskell snippet:

action :: (ParseError -> IO ()) -> Opts -> IO ()
action errorAction Opts{language, sourcePath, targetPath, autoFormat} =
  readFileUtf8 sourcePath
  >>= (pack <<$>>) . traverse format . toTargetLanguage language
  >>= either errorAction createDirAndWriteFile

  where
    createDirAndWriteFile x = createDirectoryIfMissing True finalDir *>
                              writeFileUtf8 finalPath x
    finalDir                = takeDirectory finalPath
    finalPath               = pathToLanguage language targetPath'

    targetPath' = if isDir targetPath then
                    replaceFileName targetPath (takeFileName sourcePath)
                  else
                    targetPath

    format      = if autoFormat then
                    readProcess (formatterExec language)
                                ["--stdin", finalPath]
                  else
                    pure

Converted Scala output (after formatting):

def action(x: ParseError => IO[Unit])(y: Opts): IO[Unit] =
  (x, y) match
    case (errorAction, Opts(language, sourcePath, targetPath, autoFormat)) =>
      def createDirAndWriteFile =
        createDirectoryIfMissing(true)(finalDir) *> writeFileUtf8(finalPath)(x)
      def finalDir =
        takeDirectory(finalPath)
      def finalPath =
        pathToLanguage(language, targetPath$)
      def targetPath$ =
        if isDir(targetPath) then
          replaceFileName(targetPath)(takeFileName(sourcePath))
        else targetPath
      def format =
        if autoFormat then
          readProcess(formatterExec(language))(List("--stdin", finalPath))
        else pure

      readFileUtf8(sourcePath)
      >>= (pack <<&>> _) ^ traverse(format) ^ toTargetLanguage(language)
      >>= either(errorAction)(createDirAndWriteFile)

Supported GHC Syntax Extensions

# Syntax Sugar
- LambdaCase
- MultiWayIf
- PostfixOperators

# Types
- RankNTypes
- ExplicitForAll
- ScopedTypeVariables

# Records
- DuplicateRecordFields
- NoFieldSelectors
- NamedFieldPuns
- RecordWildCards
- OverloadedRecordDot

# Type Classes
- ConstrainedClassMethods
- MultiParamTypeClasses

# Deriving 
- StandaloneDeriving
- DerivingVia
- NewTypeDeriving
- AnyClassDeriving
- DerivingStrategies