В нашей предыдущей статье «Профункторы: невоспетые герои, превосходящие монады в функциональном программировании» мы погрузились в малоизвестный мир профункторов. В качестве продолжения мы продолжаем изучение, включая мощные концепции, такие как линзы и стрелки, чтобы продемонстрировать практические преимущества профункторов над монадами. В частности, мы сосредоточены на построении конвейеров данных и управлении сложными структурами данных. С помощью реальных примеров использования и фрагментов кода мы покажем убедительную эффективность, которую эта мощная команда — профункторы, линзы и стрелки — привносит в мир программирования на 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, рассмотрите возможность использования этих инструментов в своем наборе инструментов. Вы будете приятно удивлены тем, на что они способны. Спасибо за чтение!