В нашей предыдущей статье «Профункторы: невоспетые герои, превосходящие монады в функциональном программировании» мы погрузились в малоизвестный мир профункторов. В качестве продолжения мы продолжаем изучение, включая мощные концепции, такие как линзы и стрелки, чтобы продемонстрировать практические преимущества профункторов над монадами. В частности, мы сосредоточены на построении конвейеров данных и управлении сложными структурами данных. С помощью реальных примеров использования и фрагментов кода мы покажем убедительную эффективность, которую эта мощная команда — профункторы, линзы и стрелки — привносит в мир программирования на Haskell.
Профункторы: абстракция более высокого уровня
Профункторы с их двумя функциями отображения можно рассматривать как обобщенную функцию. Они в то же время контравариантны, потому что действуют на вход, и ковариантны, потому что воздействуют на выход, что делает их идеальными для определения двунаправленных преобразований данных, помните сигнатуру профунктора p с функциями отображения: (b ->a) ->(c ->d) -> p a c-> p b d , где у нас есть две функции отображения, первая из которых контравариантна, а вторая ковариантна, и один профунктор p с двумя частями: одна действует на вход и на выходе (p b d). И другой ( p a c ), выполняющий то же самое на промежуточных данных после ввода и до того, как преобразование произойдет на выходе.
Хотя монады абстрагируют последовательности вычислительных шагов, иногда они могут не сработать в сценариях, где преобразования являются ключевыми, например, в конвейерах данных. Вот где профункторы блистают.
{-# LANGUAGE DerivingVia #-} import Data.Profunctor data Worker a b s t = Worker (s -> Either t a) (b -> t) deriving (Profunctor, Strong) via Star (Either t) {- Note: Star is a constructor of Profunctor that handles only types with kind: Type -> Type like Maybe. But Either is Type ->Type -> Type, so this is why I applied partially t before the constructor takes a. In this way we can process in two Steps a type like Either t a. -}
В этом фрагменте мы определили тип данных Worker с помощью профункторов. Этот тип данных можно использовать для определения преобразований между различными типами данных. Примечательно, что он использует монаду Либо (для обработки ошибок журнала без необходимости использования исключений), что позволяет нам обрабатывать вычисления, которые могут привести к двум различным типам значений. В этом суть профунктора: обеспечение более высокого уровня абстракции для преобразований, которые включают в себя потенциальное ветвление или условия ошибки.
Линзы и профункторы: идеальное сочетание
Линза — это первоклассная пара геттеров и сеттеров для определенного поля структуры данных. Его можно интерпретировать как профунктор, и библиотека линз в Haskell использует эту взаимосвязь для обеспечения надежного и гибкого интерфейса.
{-# LANGUAGE TemplateHaskell #-} import Control.Lens data Worker = Worker { _name :: String, _role :: String , _years :: Int, _salary :: Float } deriving Show makeLenses ''Worker worker = Worker "Jose" "Engineer" 5 70000 workerName = view name worker updatedWorker = over years (+1) worker main :: IO () main = do print workerName print updatedWorker
В этом фрагменте мы определяем тип данных Worker и используем Template Haskell для автоматического создания линз для его полей. Затем мы можем использовать функцию просмотра для извлечения значения поля и функцию over для изменения значения поля.
Стрелки, профункторы и сила композиции
Стрелки можно рассматривать как профункторы с дополнительной структурой, что позволяет создавать и отображать функции в контекстах, где монады могут быть слишком ограничивающими. Стрелки и профункторы можно использовать вместе для создания гибких композиционных конвейеров данных.
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE Arrows #-} import Control.Arrow import Control.Lens import Control.Monad.IO.Class -- again decl data Worker = Worker { _name :: String, _position :: String, _yearsExperience :: Int, _salary :: Int } deriving Show -- This line will generate again -- lenses for each field in the Worker record makeLenses ''Worker procWorker :: Kleisli IO Worker String procWorker = Kleisli $ \worker -> do liftIO $ putStrLn ("Processing worker: " ++ view name worker) return (view name worker) main :: IO () main = do let worker = Worker "Pepe" "Manager" 10 100000 runKleisli procWorker worker
Этот код определяет стрелку, которая берет Worker и выполняет над ним некоторые действия ввода-вывода. Тип Kleisli используется для переноса функций, возвращающих монадические значения, что делает их компонуемыми в виде стрелки. В этом случае функция procWorker берет Worker, выводит его имя, а затем возвращает его имя в виде строки. Затем функция runKleisli используется для применения этой функции к Worker.
В этой статье мы увидели, как профункторы, линзы и стрелки могут работать вместе для создания выразительных и мощных конвейеров данных и преобразований. Хотя монады являются важной частью экосистемы Haskell, они не всегда являются лучшим инструментом для любой работы. Профункторы, линзы и стрелки предлагают альтернативные абстракции, которые могут быть более естественными и эффективными в определенных контекстах. Итак, в следующий раз, когда вы будете строить конвейер данных или манипулировать сложными структурами данных в Haskell, рассмотрите возможность использования этих инструментов в своем наборе инструментов. Вы будете приятно удивлены тем, на что они способны. Спасибо за чтение!