Нахождение первого самого большого числа di git в строке с Haskell. ФУНКЦИОНАЛЬНЫЙ ПАРСЕР - PullRequest
0 голосов
/ 16 января 2020

Эта проблема возникла из-за моего желания создать синтаксический анализатор функций, который будет принимать любую математическую функцию с переменными n и анализировать ее в древовидную структуру, которая может быть оценена и возвращена в качестве ответа. Я хочу сделать это, потому что я пытаюсь найти лучший способ сделать что-то типа числового анализа без использования Matlab.

Я не уверен, что у кого-то еще была эта проблема, но в настоящее время у меня есть рабочий класс дерева, но у меня есть проблема с анализом функции в виде дерева. Моя первая проблема связана с моей неспособностью понять, как пройти через строку и найти наибольшее число в функции. В идеале, я бы назвал функцию createFunction, которая имеет подпись createFunction :: String -> Function, и это займет, например, f(x)=34*x+92*x-3*x или даже просто 34*x+92*x-3*x, но я не могу понять, как лучше всего это проанализировать и найти числа, так как с помощью прочитал бы просто получить 3 за 34 (я думаю.). Я думал о том, как я буду делать это обязательно, и я делал буфер, пока я не получил число, а затем прочитал число в этом буфере, или я думаю, список в этом случае, но это кажется плохим и очень не в духе функционального программирования. У меня просто проблема с концептуализацией проблемы и ее функциональным решением.

Если в Haskell есть только способ заменить x на значение x и сделать это, было бы интересно посмотреть. Я проверил несколько вещей, но ничто, казалось, не имело смысла для меня или понимало, что происходит, чтобы использовать это эффективно. Мне просто нужно, чтобы кто-то указал мне правильное направление. Я предполагаю, что регулярного выражения будет достаточно, но я не уверен. Вот мой код для всех, кому нужно представление о том, что я делаю.

Мои четкие ожидания - взять что-то вроде 34*x-4 и превратить его в дерево с узлом root, который является вычитанием. знак с левым дочерним элементом, который является знаком умножения с двумя листами 34 и x, а знак вычитания root будет иметь правый дочерний элемент 4. Все это будет оценено как таковое и заменит x на входное значение. Это может быть слишком убить, чтобы использовать дерево. Это дерево ни в коем случае не нужно для решения этой проблемы, но я подумал, что это будет наилучшим способом сделать это!

operator.hs:

module Operator (Operator (Add, Sub, Mult, Div)) where

data Operator = Add | Sub | Mult | Div
  deriving (Eq,Show)

tree.hs:

module Tree (Tree (Leaf, Branch), fromFloat) where

import Operator (Operator(Add,Sub,Mult,Div))

data Tree = Leaf Float | Branch Operator Tree Tree
  deriving (Eq,Show)

fromFloat :: Float -> Tree
fromFloat = Leaf

function.hs:

module Function(fromFloat) where
import Tree

data Function = Function String Int Tree
  deriving (Eq,Show)

1 Ответ

2 голосов
/ 18 января 2020

Довольно стандартный способ написания синтаксических анализаторов с нуля на функциональных языках заключается в написании функций, которые принимают в качестве аргумента «оставленный ввод», пытаются проанализировать начальную часть этого ввода и вернуть результат анализа плюс "оставшийся ввод". Поскольку вы обычно хотите разрешить синтаксическому анализатору «изящно провалиться» (см. Ниже), вы обычно заключаете это возвращаемое значение в Maybe или что-то в этом роде.

Например, функция синтаксического анализа, которая пытается проанализировать целое число в начале строки будет выглядеть примерно так:

import Data.Char
import Data.List

number :: String -> Maybe (Int, String)
number str = case span isDigit str of
  ([], _) ->   Nothing   -- no digits found
  (a, rest) -> Just (read a, rest)

Вот как это работает:

> number "34*x-4"
Just (34,"*x-4")
> number "x+2"
Nothing

Не так уж сложно написать синтаксический анализатор для простых полиномиальных выражений, используя это подходить. Вот синтаксический анализатор для имен односимвольных переменных:

-- read a single-character variable name
var :: String -> Maybe (Char, String)
var (x:rest) | isAlpha x = Just (x, rest)
             | otherwise = Nothing

Из этого вы можете создать синтаксический анализатор для «терминов» в форме «2 * x», «y» или «18».

-- read a "term":  "2*x" or "y" or "18"
data Term = TermPoly Int Char | TermConstant Int
  deriving (Show)
term :: String -> Maybe (Term, String)
term str
  = case number str of
      Just (n, '*':str1) -> case var str1 of
        Just (x, str2) -> Just (TermPoly n x, str2)
        Nothing -> error "expected variable"
      Just (n, str3) -> Just (TermConstant n, str3)
      Nothing -> case var str of
        Just (x, str4) -> Just (TermPoly 1 x, str4)
        Nothing -> error "expected term"

Обратите внимание, как мы используем потенциальный сбой исходного number str синтаксического анализатора (т. Е. Ветвь Nothing самого верхнего case), чтобы попытаться проанализировать альтернативу переменной без коэффициента ( например, "y"). Вот почему полезно, чтобы синтаксические анализаторы «изящно терпели неудачу».

Теперь мы можем написать синтаксический анализатор для сумм и различий в терминах. Неловкое использование вспомогательной функции go здесь гарантирует, что "2*x-4*y+3" будет проанализирован как (2*x-4*y)+3 вместо 2*x-(4*y+3), что может произойти, если вы используете более очевидный рекурсивный метод определения poly без go helper:

-- read a polynomial as a sum of terms
data Poly = Single Term | Plus Poly Term | Minus Poly Term
  deriving (Show)
poly :: String -> Maybe (Poly, String)
poly str = case term str of
  Just (t, rest) -> go (Single t) rest
  Nothing -> error "expected term"
  where go :: Poly -> String -> Maybe (Poly, String)
        go p ('+':str1) = case term str1 of
          Just (t', str2) -> go (Plus p t') str2
          Nothing -> error "expected term"
        go p ('-':str1) = case term str1 of
          Just (t', str3) -> go (Minus p t') str3
          Nothing -> error "expected term"
        go p str4 = Just (p, str4)

С приведенным выше кодом вы получите:

> poly "34*x-4"
Just (Minus (Single (TermPoly 34 'x')) (TermConstant 4),"")
> poly "34*x+92*x-3*x"
Just (Minus (Plus (Single (TermPoly 34 'x')) (TermPoly 92 'x')) (TermPoly 3 'x'),"")
> poly "x+y"
Just (Plus (Single (TermPoly 1 'x')) (TermPoly 1 'y'),"")

Он не обрабатывает ведущий знак минус, хотя:

> poly "-2*x+y"
*** Exception: expected term
CallStack (from HasCallStack):
  error, called at PolyParser.hs:27:20 in main:Main

он также не обрабатывает x*5 или 2*5*x или скобки, или пробелы, или множество других случаев.

Кроме того, самый большой недостаток этого подхода заключается в том, насколько сложно объединять простые анализаторы компонентов в большие анализаторы. Например, term на самом деле концептуально очень прост: проанализируйте или number раз var, или одинокий number или одинокий var, но у нас есть все эти сложные операторы case для объединения number и var и проанализируйте символ '*'. Именно здесь важными становятся «комбинаторы синтаксического анализа» или «синтаксические анализаторы monadi c», так как они предоставляют простой и простой синтаксис для объединения синтаксических анализаторов.

По этой же причине опытный программист Haskell не пишет парсеры с нуля. Есть много отличных и мощных библиотек monadi c для анализа. Они занимают некоторое время, чтобы научиться использовать, но они того стоят. Достаточно полнофункциональный синтаксический анализатор Megaparse c для языка выражений занимает около 30 минут для записи в 60 с лишним строк и может обрабатывать сложные выражения (как пример в функции main ниже):

import Data.Void
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
import Control.Monad.Combinators.Expr

type Parser = Parsec Void String

-- standard boilerplate for space handling
spaces :: Parser ()
spaces = L.space space1 empty empty
lexeme :: Parser a -> Parser a
lexeme = L.lexeme spaces
symbol :: String -> Parser String
symbol = L.symbol spaces

-- parse a number
number :: Parser Float
number = lexeme $ try L.float <|> fromIntegral <$> L.decimal

-- parse an identifier (e.g., "x_4")
identifier :: Parser String
identifier = lexeme $ (:) <$> (letterChar <|> char '_') <*> many (alphaNumChar <|> char '_')

-- abstract syntax tree for expressions
data Expr
  = Num Float
  | Var String
  | Negate Expr
  | BinOp BinOp Expr Expr
  deriving (Show)
data BinOp
  = Add | Sub | Mult | Div | Power
  deriving (Show)

-- parse a "term": number, identifier, or expression in parentheses
term :: Parser Expr
term = Num <$> number
  <|> Var <$> identifier
  <|> between (symbol "(") (symbol ")") expr

-- parse an expression combining terms with operators
expr :: Parser Expr
expr = makeExprParser term
  [ [binaryR "^" Power]
  , [Prefix (Negate <$ symbol "-")]
  , [binary "*" Mult, binary "/" Div]
  , [binary "+" Add, binary "-" Sub]
  ]
  where binary name f = InfixL (BinOp f <$ symbol name)
        binaryR name f = InfixR (BinOp f <$ symbol name)

-- parse a whole string as an expression
fullExpr :: Parser Expr
fullExpr = spaces *> expr <* eof

main = parseTest fullExpr "(pi*r^2 - 4*pi*r) / (c^2 - a^2 - b^2)"

Как я уже сказал, до того момента, когда вы сможете писать такие парсеры, требуется довольно много времени, но стоит пройтись по некоторым учебникам, даже если вы еще не достигли того момента, когда сможете писать парсеры все по своему. Некоторые ресурсы, которые могут быть полезны для начала работы:

...