morfeusz-0.3.0: NLP/Morfeusz.hsc
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DeriveFunctor #-}
-- | The module provides 'asDAG', 'asPaths' and 'asPath' wrapper functions
-- which use the Morfeusz library to analyse input sentences.
-- The first one represents analysis results as a directed acylic graph
-- (DAG) with edges labeled with 'Token's. The DAG representation is needed
-- when the input word has multiple correct segmentations.
--
-- >>> :m NLP.Morfeusz
-- >>> :set -XOverloadedStrings
-- >>> let wordAsDAG = head . lefts . asDAG
-- >>> mapM_ print . wordAsDAG $ "miałem"
-- Edge {from = 0, to = 1, label = Token {orth = "mia\322", interps = [Interp {base = "mie\263", msd = "praet:sg:m1.m2.m3:imperf"}]}}
-- Edge {from = 0, to = 2, label = Token {orth = "mia\322em", interps = [Interp {base = "mia\322", msd = "subst:sg:inst:m3"}]}}
-- Edge {from = 1, to = 2, label = Token {orth = "em", interps = [Interp {base = "by\263", msd = "aglt:sg:pri:imperf:wok"}]}}
--
-- Alternatively you can use the 'asPaths' function to see all paths instead of a DAG.
--
-- >>> let wordAsPaths = head . lefts . asPaths
-- >>> mapM_ print . wordAsPaths $ "miałem"
-- [Token {orth = "mia\322em", interps = [Interp {base = "mia\322", msd = "subst:sg:inst:m3"}]}]
-- [Token {orth = "mia\322", interps = [Interp {base = "mie\263", msd = "praet:sg:m1.m2.m3:imperf"}]},Token {orth = "em", interps = [Interp {base = "by\263", msd = "aglt:sg:pri:imperf:wok"}]}]
--
-- The last analysis function, 'asPath', takes paths extracted using the 'asPaths' function
-- and arbitrarily chooses one of them. While it returns only part of the analysis result,
-- it is also the easiest to use.
--
-- >>> mapM_ print . asPath $ "zdanie ze spacjami"
-- Left (Token {orth = "zdanie", interps = [Interp {base = "zda\263", msd = "ger:sg:nom.acc:n2:perf:aff"},Interp {base = "zdanie", msd = "subst:sg:nom.acc.voc:n2"}]})
-- Right " "
-- Left (Token {orth = "ze", interps = [Interp {base = "z", msd = "prep:inst:wok"},Interp {base = "z", msd = "prep:gen.acc:wok"}]})
-- Right " "
-- Left (Token {orth = "spacjami", interps = [Interp {base = "spacja", msd = "subst:pl:inst:f"}]})
--
-- Use the 'lefts' function when you want to remove spaces from the result.
module NLP.Morfeusz
(
-- * Types
DAG
, Edge (..)
, Token (..)
, Interp (..)
, Space
-- * Sentence analysis
, asDAG
, asPaths
, asPath
-- * Utilities
, toPaths
, mapL
, concatL
, concatMapL
, module Data.Either
) where
import System.IO.Unsafe (unsafePerformIO)
import Control.Applicative ((<$>), (<*>))
import Control.Monad (when)
import Control.Monad.State (evalState, put, get)
import Data.Function (on)
import Data.List (groupBy)
import Data.Char (isSpace)
import Data.Either
import qualified Data.Map as M
import qualified Data.ByteString as B
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Foreign hiding (unsafePerformIO)
import Foreign.C.Types
import Foreign.C.String (CString)
import NLP.Morfeusz.Lock (lock)
#include "morfeusz.h"
-- | Morfeusz options
newtype MorfOption = MorfOption { unMorfOption :: CInt }
deriving (Eq, Show)
newtype Encoding = Encoding { unEncoding :: CInt }
deriving (Eq, Show)
newtype WhiteSpace = WhiteSpace { unWhiteSpace :: CInt }
deriving (Eq, Show)
#{ enum MorfOption, MorfOption
, encoding = MORFOPT_ENCODING
, whitespace = MORFOPT_WHITESPACE }
#{ enum Encoding, Encoding
, utf8 = MORFEUSZ_UTF_8
, iso8859_2 = MORFEUSZ_ISO8859_2
, cp1250 = MORFEUSZ_CP1250
, cp852 = MORFEUSZ_CP852 }
#{ enum WhiteSpace, WhiteSpace
, skip_whitespace = MORFEUSZ_SKIP_WHITESPACE
, keep_whitespace = MORFEUSZ_KEEP_WHITESPACE }
-- | Set the encoding.
setEncoding :: Encoding -> IO Bool
setEncoding enc = (1 ==) <$>
c_morfeusz_set_option (unMorfOption encoding) (unEncoding enc)
-- | Set the Morfeusz whitespace option.
setSpace :: WhiteSpace -> IO Bool
setSpace spc = (1 ==) <$>
c_morfeusz_set_option (unMorfOption whitespace) (unWhiteSpace spc)
-- | A directed edge with label of type @a@ between nodes of type 'Int'.
data Edge a = Edge
{ from :: Int
, to :: Int
, label :: a }
deriving (Eq, Ord, Show, Functor)
-- | Raw morphosyntactic interpretation as presented by the Morfeusz.
data RawInterp = RawInterp
{ _orth :: T.Text
, _base :: Maybe T.Text
, _msd :: Maybe T.Text }
deriving (Eq, Ord, Show)
-- | A token with a list of recognized interpretations. If the list of
-- interpretations is empty, the token is unknown to the Morfeusz.
data Token = Token
{ orth :: T.Text
, interps :: [Interp] }
deriving (Show)
-- | An interpretation of the word.
data Interp = Interp
{ base :: T.Text
, msd :: T.Text }
deriving (Show)
-- | A space.
type Space = T.Text
-- | We only provide the peek functionality.
instance Storable (Edge RawInterp) where
sizeOf _ = (#size InterpMorf)
alignment _ = alignment (undefined :: CString) -- or CInt ?
peek ptr = do
from <- getInt ((#peek InterpMorf, p) ptr)
to <- getInt ((#peek InterpMorf, k) ptr)
(orth, base, msd) <- if from == -1
then return ("", Nothing, Nothing)
else (,,)
<$> getText ((#peek InterpMorf, forma) ptr)
<*> getTextMaybe ((#peek InterpMorf, haslo) ptr)
<*> getTextMaybe ((#peek InterpMorf, interp) ptr)
return $ Edge from to (RawInterp orth base msd)
where
getInt = fmap fromIntegral :: IO CInt -> IO Int
getText cStrIO = peekText =<< cStrIO
getTextMaybe cStrIO = cStrIO >>= \cStr -> do
if cStr == nullPtr
then return Nothing
else Just <$> peekText cStr
peekText xs = T.decodeUtf8 <$> B.packCString xs
foreign import ccall unsafe "morfeusz_analyse"
-- InterpMorf *morfeusz_analyse(char *tekst)
c_morfeusz_analyse :: CString -> IO (Ptr (Edge RawInterp))
foreign import ccall unsafe "morfeusz_set_option"
-- int morfeusz_set_option(int option, int value)
c_morfeusz_set_option :: CInt -> CInt -> IO CInt
-- | A DAG with annotated edges.
type DAG a = [Edge a]
-- | Analyse the word and output raw Morfeusz results.
analyse :: T.Text -> DAG RawInterp
analyse word = run $ \cword -> lock $ do
_ <- setEncoding utf8
_ <- setSpace skip_whitespace
interp_ptr <- c_morfeusz_analyse cword
when (interp_ptr == nullPtr) (fail $ "analyse: null pointer")
retrieve 0 interp_ptr
where
run = unsafePerformIO . B.useAsCString (T.encodeUtf8 word)
retrieve k ptr = do
x <- peekElemOff ptr k
if from x == -1
then return []
else (:) <$> return x <*> retrieve (k + 1) ptr
-- | Translate the DAG of raw Morfeusz interpretations to
-- DAG labeled with tokens.
properDAG :: DAG RawInterp -> DAG Token
properDAG dag =
[Edge p q t | ((p, q), t) <- M.toAscList m]
where
m = M.fromListWith (<>) [((p, q), fromRaw r) | Edge p q r <- dag]
fromRaw (RawInterp o (Just b) (Just m)) = Token o [Interp b m]
fromRaw (RawInterp o _ _) = Token o []
Token orth xs <> Token _ ys = Token orth (xs ++ ys)
-- | Analyse the input sentence as a DAG of tokens interspersed by spaces.
-- The sentence is divided on spaces first and only individual words are
-- delivererd to Morfeusz library for morphosyntactic analysis.
asDAG :: T.Text -> [Either (DAG Token) Space]
asDAG =
flip evalState 0 . mapM updateIxs . map mkElem . T.groupBy cmp
where
cmp x y = isSpace x == isSpace y
mkElem x
| T.any isSpace x = Right x
| otherwise = Left . properDAG . analyse $ x
updateIxs (Right x) = return (Right x)
updateIxs (Left xs) = Left <$> updateDAG xs
updateDAG xs = do
n <- get
let m = maximum . map to $ xs
ys = map (shift n) xs
put (n + m)
return ys
shift k Edge{..} = Edge (from + k) (to + k) label
-- | Retrieve all paths from the root to leaves.
toPaths :: DAG a -> [[a]]
toPaths dag =
doIt .fst . M.findMin $ m
where
m = M.fromListWith (++) [(from e, [e]) | e <- dag]
doIt p = case M.lookup p m of
Just es -> [(label e : path) | e <- es, path <- doIt (to e)]
Nothing -> [[]]
-- | Similar to the 'asDAG' function but instead of a token DAG it returns
-- all DAG paths (using the 'toPaths' function) for each word in the
-- input sentence.
asPaths :: T.Text -> [Either [[Token]] Space]
asPaths = mapL toPaths . asDAG
-- | Analyse the input sentence and arbitrarily choose one path
-- from the output DAG.
asPath :: T.Text -> [Either Token Space]
asPath =
let hd [] = error "asPath.head: empty list"
hd (x:xs) = x
in concatMapL hd . asPaths
-- | Map the function over left elements.
mapL :: (a -> a') -> [Either a b] -> [Either a' b]
mapL f =
let g (Left x) = Left (f x)
g (Right y) = Right y
in map g
-- | Concatenate left elements.
concatL :: [Either [a] b] -> [Either a b]
concatL =
let liftL (Left xs) = [Left x | x <- xs]
liftL (Right y) = [Right y]
in concat . map liftL
-- | Map the function over left elements and concatenate results.
concatMapL :: (a -> [b]) -> [Either a c] -> [Either b c]
concatMapL f = concatL . mapL f