Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Аппликативное программирование или как парсить ...

Аппликативное программирование или как парсить SQL на чистом Haskell

Функциональное программирование использует подходы, не похожие на те, что мы применяем каждый день: функции чистые, переменных нет, а во многих языках нет даже циклов в привычном нам смысле. Из–за этого возникают специфические проблемы, которые приходится решать, и часто эти решения реализованы довольно интересно. Однако в докладе речь пойдет о другом: мы посмотрим на то, как применение сильных сторон ФП поможет нам построить очень удобную в применении штуку. Мы соберем из подручных средств очень простой, но мощный парсер.

Первая часть доклада — про основы синтаксиса Haskell. Во второй части — поговорим про функторы, из которых мы будем собирать нашу программу. В финале — мы сделаем парсер общего назначения в аппликативном стиле и научим его разбирать небольшое подмножество языка SQL.

Dmitry Tsepelev

July 16, 2022
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

  1. DmitryTsepelev 🌆 Зачем это все? • ФП — это весело;

    • многие штуки доезжают из ФП в модные языки через 10-30 лет, можно заглянуть в будущее 🔮; • есть свои сложности и интересные способы их решать (спросите меня про линзы!); • некоторые вещи значительно проще, чем в ООП; • сам Haskell простой как 🚪, сложные концепции надстраиваются (одну штуку мы увидим прямо сейчас!). 3
  2. DmitryTsepelev 🌆 Типы 5 Int - - целое число String

    - - строка type IntList = [Int] - - список type String = [Char] - - строки это списки букв (Int, Int) - - пара
  3. DmitryTsepelev 🌆 Функции • все функции чистые; • состояние не

    мутируется; • функция принимает 0 или более аргументов и возвращает одно значение; • могут вызываться в инфиксной форме с помощью ``. 6 sum : : Int - > Int - > Int sum x y = x + y sum 3 6 - - 9 3 `sum` 6 - - 9
  4. DmitryTsepelev 🌆 Функции: инфиксная форма • in fi x делает

    функцию инфиксной по умолчанию; • первый аргумент — приоритет функции; • могут вызываться в инфиксной форме с помощью (). 7 sum : : Int - > Int - > Int sum x y = x + y inf i x 6 /\/\ (/\/\) = sum 1 /\/\ 2 - - 3 (/\/\) 1 2 - - 3
  5. DmitryTsepelev 🌆 Изменение приоритета операций: $ • выполняется сначала правая

    часть, затем левая; • позволяет избавиться от лишних скобок; • не является частью синтаксиса языка. 8 show 1 + 1 - - No instance for (Num String) arising from a use of + show (1 + 1) - - 2 inf i xr 0 $ ($) : : (a - > b) - > a - > b f $ x = f x show $ 1 + 1 - - 2
  6. DmitryTsepelev 🌆 Частичное применение функции • можно считать, что функция

    всегда принимает и возвращает один аргумент; • если аргументов больше — вернется новая функция; • такая операция называется каррированием. 9 sum : : Int - > (Int - > Int) sum x y = x + y sum3 : : Int - > Int sum3 = sum 3 sum3 6 - - 9
  7. DmitryTsepelev 🌆 Композиция • функция . (точка) позволяет из двух

    функций создать одну, вызывающую обе по очереди; • можно убрать немного скобок. 10 inf i xr 9 . (.) : : (b - > c) - > (a - > b) - > a - > c (.) f g a = f (g a) succ . (*2) $ 3 - - 7
  8. DmitryTsepelev 🌆 Алгебраические типы умножение (product/И): тип, состоящий из нескольких

    других типов 11 data User = User String String User "[email protected]" "Jonh Doe" сложение (sum/ИЛИ): тип, который может принимать значения разных типов data UserType = Admin | User | Guest
  9. DmitryTsepelev 🌆 Алиасы типов • для читабельности можно создать алиасы

    (Email, Name); • можно явно указать названия в сигнатуре чтобы сгенерировать функции. 12 type Email = String type Name = String data User = User Email Name User "[email protected]" "Jonh Doe" data User2 = User { email : : String, name : : String }
  10. DmitryTsepelev 🌆 Параметризованные типы • параметризованный тип может содержать значение

    произвольного типа; • на тип можно накладывать ограничения; • Just в примере ниже — конструктор. 13 data Maybe a = Just a | Nothing Just 3
  11. DmitryTsepelev 🌆 Pattern matching и guard conditions • можно проверять

    тип значения и разбирать его в теле функции; • можно использовать guard conditions чтобы проверять более сложные условия. 14 sayHi : : Maybe String - > String sayHi (Just name) = "Hi, " + + name + + "!" sayHi Nothing = "Hi!" sayHi mName | isJust mName = "Hi, " + + (fromJust name) + + "!" | otherwise = "Hi!"
  12. DmitryTsepelev 🌆 Классы типов • аналог интерфейсов в ООП; •

    содержит функции, которые должен реализовывать тип, который реализует данный класс типов; • может содержать реализации по умолчанию. 15 class Num a where (+), (-), (*) : : a - > a - > a negate : : a - > a abs : : a - > a signum : : a - > a fromInteger : : Integer - > a x - y = x + negate y negate x = 0 - x
  13. DmitryTsepelev 🌆 Реализация классов типов • нужно объявить все обязательные

    методы; • если класс типов содержит минимальную реализацию, то его можно объявить в сокращенной форме. 16 instance Num Integer where (+) = integerAdd (-) = integerSub (*) = integerMul negate = integerNegate fromInteger i = i abs = integerAbs signum = integerSignum data User = User { email : : String } deriving (Show) User "[email protected]" - - User {email = "[email protected]"}
  14. DmitryTsepelev 🌆 Более гибкие функции • вместо конкретных типов можно

    использовать переменные–типы (type variables); • при вызове будут использованы конкретные типы; • для того, чтобы ограничить подходящие типы, можно использовать классы типов (перед = > ). 17 sum : : Num a = > a - > a - > a sum x y = x + y sum 3 6 - - 9
  15. DmitryTsepelev 🌆 Работа со значением в коробке 19 type UserID

    = Int type Email = String data Maybe a = Just a | Nothing deriving (Show) getEmail : : UserID - > Maybe Email getEmail 42 = Just "[email protected]" getEmail _ = Nothing formatEmail : : Maybe Email - > Maybe Email formatEmail (Just email) = Just $ "Email: " + + email formatEmail Nothing = Nothing formatEmail $ getEmail 42 - - Just "Email: [email protected]"
  16. DmitryTsepelev 🌆 Класс типов Functor 20 class Functor f where

    fmap : : (a - > b) - > f a - > f b • преобразовывает значение в коробке с учетом типа коробки.
  17. DmitryTsepelev 🌆 Реализация Functor для Maybe 21 data Maybe a

    = Just a | Nothing deriving (Show) instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing type UserID = Int type Email = String getEmail : : UserID - > Maybe Email getEmail 42 = Just "[email protected]" getEmail _ = Nothing formatEmail : : Maybe Email - > Maybe Email formatEmail = fmap (\email - > "Email: " + + email) • если значение Just — происходит распаковка, применение функции и запаковка; • Nothing остается без изменений.
  18. DmitryTsepelev 🌆 <$> — инфиксный fmap 22 data Maybe a

    = Just a | Nothing deriving (Show) instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing inf i x 4 < $ > ( < $ > ) = fmap type UserID = Int type Email = String getEmail : : UserID - > Maybe Email getEmail 42 = Just "[email protected]" getEmail _ = Nothing formatEmail : : Maybe Email - > Maybe Email formatEmail maybeEmail = ("Email: " + + ) < $ > maybeEmail
  19. DmitryTsepelev 🌆 Реализация Functor для пары 23 • у пары

    есть семантика; • функция применяется ко второму элементу. instance Functor ((,) a) where fmap f (x,y) = (x, f y)
  20. DmitryTsepelev 🌆 Аппликативные функторы • fmap принимает функцию с одним

    аргументом; • у нас есть каррирование, поэтому все функции подходят; • что будет, если передать функцию с двумя аргументами? 25 (+1) < $ > Just 42 - - Just 43 (+) < $ > Just 42 - - Just (42 +)
  21. DmitryTsepelev 🌆 Класс типов Applicative • теперь мы можем упороться

    и применить функцию в коробке к значению в коробке; • pure заворачивает значение в минимально простой контейнер; • < * > достает значение из коробки слева и применяет к значению в коробке справа, затем кладет всё в исходную коробку (если сможет!). 26 class (Functor f) = > Applicative f where pure : : a - > f a ( < * > ) : : f (a - > b) - > f a - > f b
  22. DmitryTsepelev 🌆 Реализация Applicative Functor для Maybe 27 data Maybe

    a = Just a | Nothing deriving (Show) instance Functor Maybe where fmap f (Just x) = Just (f x) fmap f Nothing = Nothing inf i xl 4 < $ > ( < $ > ) = fmap instance Applicative Maybe where pure = Just Nothing < * > _ = Nothing (Just f) < * > something = f < $ > something formatEmail : : Maybe Email - > Maybe Email formatEmail maybeEmail = pure ( + + ) < * > maybeEmail < * > Just "Email: " • pure — кладет значение в Just; • <*> вернет Nothing если Nothing будет слева либо справа; • иначе — обычный fmap.
  23. DmitryTsepelev 🌆 Каррирование в аппликативном стиле • можно создавать функции,

    работающие только с «хорошим сценарием»; • плохой сценарий будет обработан реализацией аппликативного функтора для используемого типа. 28 sum3 : : Num n = > n - > n - > n - > n sum3 x y z = x + y + z pure sum3 < * > Just 3 < * > Just 2 < * > Just 4 - - Just 9 pure sum3 < * > Just 3 < * > Nothing < * > Just 4 - - Nothing
  24. DmitryTsepelev 🌆 *> <* • класс типов Applicative содержит дополнительные

    операции, которые могут быть выведены из <*>; • *> выполняет операцию, но игнорирует значение левого аргумента; • <* выполняет операцию, но игнорирует значение правого аргумента. 29 pure (42+) * > Just 2 - - Just 2 pure (42+) * > Nothing - - Nothing Nothing * > Just 2 - - Nothing pure (42+) < * Just 2 - - Just (42+) Nothing < * Just 2 - - Nothing pure (42+) < * Nothing - - Just (42+)
  25. DmitryTsepelev 🌆 Простейший парсер 31 data Parser a = Parser

    { parse : : String - > Either String (String, a) } satisfy : : (Char - > Bool) - > Parser Char satisfy pr = Parser f where f "" = Left "unexpected end of input" f (c:cs) = if pr c then Right (cs, c) else Left ("unexpected " + + [c]) anyChar : : Parser Char anyChar = satisfy (const True) parse anyChar "ABC" - - Right ("BC",'A')
  26. DmitryTsepelev 🌆 Простейший парсер 32 import Data.List (isPref i xOf)

    data Parser a = Parser { parse : : String - > Either String (String, a) } char : : Char - > Parser Char char p = satisfy ( = = p) parse (char 'A') "ABC" - - Right ("BC",'A') parse (char 'B') "ABC" - - Left "unexpected A” string : : String - > Parser String string str = Parser f where f s | str `isPref i xOf` s = Right (drop (length str) s, str) | otherwise = Left $ "unexpected " + + s + + ", expected " + + str parse (string "AB") "ABC" - - Right ("C","AB")
  27. DmitryTsepelev 🌆 Реализуем функтор 33 data Parser a = Parser

    { parse : : String - > Either String (String, a) } instance Functor Parser where fmap f (Parser p) = Parser $ fmap (fmap f) . p parse (toLower < $ > char 'A') "ABC" - - Right ("BC",'a')
  28. DmitryTsepelev 🌆 Реализуем аппликацию 34 data Parser a = Parser

    { parse : : String - > Either String (String, a) } instance Applicative Parser where pure a = Parser $ \s - > Right (s, a) pf < * > pv = Parser $ \s - > case parse pf s of Right (s', g) - > case parse pv s' of Right (s'', a) - > Right (s'', g a) Left e - > Left e Left e - > Left e
  29. DmitryTsepelev 🌆 Реализуем аппликацию 35 data Parser a = Parser

    { parse : : String - > Either String (String, a) } instance Applicative Parser where pure a = Parser $ \s - > Right (s, a) pf < * > pv = Parser $ \s - > case parse pf s of Right (s', g) - > case parse pv s' of Right (s'', a) - > Right (s'', g a) Left e - > Left e Left e - > Left e
  30. DmitryTsepelev 🌆 Реализуем аппликацию 36 data Parser a = Parser

    { parse : : String - > Either String (String, a) } instance Applicative Parser where pure a = Parser $ \s - > Right (s, a) pf < * > pv = Parser $ \s - > case parse pf s of Right (s', g) - > case parse pv s' of Right (s'', a) - > Right (s'', g a) Left e - > Left e Left e - > Left e
  31. DmitryTsepelev 🌆 Реализуем аппликацию 37 data Parser a = Parser

    { parse : : String - > Either String (String, a) } instance Applicative Parser where pure a = Parser $ \s - > Right (s, a) pf < * > pv = Parser $ \s - > case parse pf s of Right (s', g) - > case parse pv s' of Right (s'', a) - > Right (s'', g a) Left e - > Left e Left e - > Left e
  32. DmitryTsepelev 🌆 Реализуем аппликацию 38 data Parser a = Parser

    { parse : : String - > Either String (String, a) } instance Applicative Parser where pure a = Parser $ \s - > Right (s, a) pf < * > pv = Parser $ \s - > case parse pf s of Right (s', g) - > case parse pv s' of Right (s'', a) - > Right (s'', g a) Left e - > Left e Left e - > Left e
  33. DmitryTsepelev 🌆 Реализуем аппликацию 39 parse (pure ( : )

    < * > char 'A' < * > string "BC") "ABC" - - Right ("","ABC") parse (char 'A' * > string "BC") "ABC" - - Right ("","BC") parse (char 'A' < * string "BC") "ABC" - - Right ("",'A')
  34. DmitryTsepelev 🌆 Класс типов Alternative 40 import Control.Applicative hiding (many)

    import Data.Either (isRight) data Parser a = Parser { parse : : String - > Either String (String, a) } instance Alternative Parser where empty = Parser $ \s - > Left $ "unexpected " + + s p < | > q = Parser f where f s = let ps = parse p s in if isRight ps then ps else parse q s parse (char 'A' < | > char 'B') "AB" - - Right ("B",'A') parse (char 'A' < | > char 'B') "BA" - - Right ("A",'B')
  35. DmitryTsepelev 🌆 Комбинаторы 41 import Control.Applicative hiding (many) import Data.Either

    (isRight) data Parser a = Parser { parse : : String - > Either String (String, a) } instance Alternative Parser where empty = Parser $ \s - > Left $ "unexpected " + + s p < | > q = Parser f where f s = let ps = parse p s in if isRight ps then ps else parse q s many : : Parser a - > Parser [a] many p = pure ( : ) < * > p < * > many p < | > pure [] parse (many (char 'A')) "AAABC" - - Right ("BC","AAA") parse (many (char 'A')) "BC" - - Right ("BC","") many1 : : Parser a - > Parser [a] many1 p = pure ( : ) < * > p < * > many p parse (many1 (char 'A')) "AAABC" - - Right ("BC","AAA") parse (many1 (char 'A')) "BC" - - Left "unexpected B"
  36. DmitryTsepelev 🌆 SQL: select 42 whitespace : : Parser String

    whitespace = many (char ' ') sepBy : : Parser a - > Parser sep - > Parser [a] sepBy p sep = pure (flip ( : )) < $ > many (p < * sep) < * > p alphaNum : : Parser Char alphaNum = satisfy isAlphaNum tableNameP : : Parser String tableNameP = many1 (alphaNum < | > char '.') selectP : : Parser [String] selectP = string "select" * > whitespace * > (tableNameP `sepBy` (char ',' < * whitespace)) parse selectP "select movies.id, movies.title" - - Right ("",["movies.title","movies.id"])
  37. DmitryTsepelev 🌆 SQL: from 43 fromP : : Parser String

    fromP = whitespace * > string "from" * > whitespace * > many1 alphaNum parse fromP "from movies" - - Right ("","movies")
  38. DmitryTsepelev 🌆 SQL: join 44 data Join = Join String

    String String deriving (Show) joinP : : Parser Join joinP = Join < $ > (whitespace * > string "join" * > whitespace * > many1 alphaNum < * whitespace) < * > (string "on" * > whitespace * > tableNameP < * whitespace) < * > (char '=' * > whitespace * > tableNameP) parse joinP "join directors on movies.directorId = directors.id” - - Right ("",Join "directors" "movies.directorId" "directors.id")
  39. DmitryTsepelev 🌆 SQL: joins 45 data Join = Join String

    String String deriving (Show) optionMaybe : : Parser a - > Parser (Maybe a) optionMaybe p = Parser $ \s - > let ps = parse p s in case ps of Right (s', a) - > if s' / = s then Right (s', Just a) else Right (s', Nothing) Left _ - > Right (s, Nothing) joinsP : : Parser (Maybe [Join]) joinsP = whitespace * > optionMaybe (many joinP)
  40. DmitryTsepelev 🌆 SQL: joins 46 let query = "join directors

    on movies.directorId = directors.id join moviesActors on moviesActors.movieId = moviesId" parse joinsP query - - Right ("",Just [Join "directors" "movies.directorId" "directors.id",Join "moviesActors" "moviesActors.movieId" "moviesId"]) parse joinsP "" - - Right ("",Nothing)
  41. DmitryTsepelev 🌆 SQL 47 runParser : : Parser a -

    > String - > a runParser p s | Right ("", a) < - parse p s = a | otherwise = error "failed to run parser" data Query = Query { selection : : [String], from : : String, joins : : Maybe [Join] } deriving (Show) sqlP : : Parser Query sqlP = Query < $ > selectP < * > fromP < * > joinsP runParser sqlP "select movies.title, movies.createdAt from movies join directors on directors.id = movies.directorId" - - Query {selection = ["movies.createdAt","movies.title"], from = "movies", joins = Just [Join "directors" "directors.id" "movies.directorId"]}
  42. DmitryTsepelev 🌆 Выводы • ФП — это весело (да?); •

    все используют функторы; • с помощью хитрых операций с функциями можно получать интересное поведение; • аппликативные функторы позволяют выстраивать цепочки вычислений без обработки ошибок в процессе. 48
  43. DmitryTsepelev 🌆 Куда пойти дальше • Learn You a Haskell

    (http:/ /learnyouahaskell.com); • Функциональное программирование на языке Haskell (https:/ / stepik.org/course/75/syllabus). 49
  44. Спасибо! @dmitrytsepelev 🌎 dmitrytsepelev.dev Надо что–то спросить 🤔 • что

    там за эмоджи слева сверху было? 🌇 • покажи фокус с долларом • почему реализация Applicative для парсера была страшная, как смерть водолаза? • а что там про монады было? • а правда ли что главное чтобы типы сошлись?
  45. DmitryTsepelev 🌆 Фокус с долларом 52 zip [1, 3] [2,

    4] - - [[1, 2], [3, 4]] zipWith (+) [1, 3] [2, 4] - - [4, 6] [(+4), (*5)] [3, 7] как вызвать функции из массива слева с аргументом справа? sum $ zipWith ($) [(+4), (*5)] [3, 7] - - 42 sum [(+4) $ 3, (*5) $ 7]
  46. DmitryTsepelev 🌆 Реализуем аппликацию 53 data Parser a = Parser

    { parse : : String - > Either String (String, a) } instance Applicative Parser where pure a = Parser $ \s - > Right (s, a) pf < * > pv = Parser $ \s - > case parse pf s of Right (s', g) - > case parse pv s' of Right (s'', a) - > Right (s'', g a) Left e - > Left e Left e - > Left e
  47. DmitryTsepelev 🌆 Applicative Parser на монадах: bind 54 data Parser

    a = Parser { parse : : String - > Either String (String, a) } instance Applicative Parser where pure a = Parser $ \s - > Right (s, a) pf < * > pv = Parser $ \s - > parse pf s > > = \(s', g) - > parse pv s' > > = \(s'', a) - > Right (s'', g a)
  48. DmitryTsepelev 🌆 Applicative Parser на монадах: do–нотация 55 data Parser

    a = Parser { parse : : String - > Either String (String, a) } instance Applicative Parser where pure a = Parser $ \s - > Right (s, a) pf < * > pv = Parser $ \s - > do (s', g) < - parse pf s (s'', a) < - parse pv s' return (s'', g a)
  49. DmitryTsepelev 🌆 many вообще не зависит от наших парсеров! 56

    many : : Parser a - > Parser [a] many p = ( : ) < $ > p < * > many p < | > pure []
  50. DmitryTsepelev 🌆 many вообще не зависит от наших парсеров! 57

    many : : Alternative t = > t a - > t [a] many p = ( : ) < $ > p < * > many p < | > pure []
  51. DmitryTsepelev 🌆 Магия типов: pure f <*> g == f

    <$> g 58 pure ( : ) < * > char 'A' ( : ) : : a - > [a] - > [a] pure : : a - > Parser a pure : : Char - > Parser a Parser (Char - > [Char] - > [Char]) < * > Parser Char ( < * > ) : : Parser (a - > b) - > Parser a - > Parser b ( < * > ) : : Parser (Char - > [Char]) - > Parser Char - > Parser [Char] Parser ([Char] - > [Char]) fmap : : (a - > b) - > Parser a - > Parser b fmap : : (Char - > [Char]) - > Parser Char - > Parser [Char] (Char - > [Char] - > [Char]) < $ > Parser Char Parser ([Char] - > [Char]) ( : ) < $ > char 'A'