Статьи

Haskell с точки зрения разработчика OO, часть 3

Сегодня я собираюсь обработать набор структурированных данных, используя Haskell, испорченный многолетним опытом Smalltalk, C ++, Java и C #. Я следил за книгой Real World Haskell , которую я выбрал по нескольким причинам:

  • Из всего, что я знаю о функциональном программировании, Haskell представляется относительно «чистым» языком FP, и
  • Книга очень всеобъемлющая, охватывает темы FP, представляющие интерес для углубленного изучения.

В то время как есть краткие введения в FP или хорошие книги по гибридным языкам OO / FP, я предпочитаю полностью порвать с OO и быстро овладеть FP.

Структурированные данные

Я собираюсь обработать набор событий, сгенерированных Java-программой (они могут быть сгенерированы любой программой, но этот выбор позволяет мне использовать один из моих текущих проектов). Каждое событие состоит из:

  • Временная метка, время эпохи, измеренное в обычных миллисекундах Java
  • Полное имя класса Java
  • Номер строки в классе, в котором генерируется событие
  • Краткое описательное сообщение
  • Список пар ключ-значение

Пары ключ-значение аналогичны свойствам сообщения JMS. В моем приложении я предполагаю, что некоторые ключи присутствуют в
каждом сообщении для целей фильтрации сообщений во время обработки.

Если вы тоже приходите на Haskell или FP в целом из OO-фона, вам не нужно видеть, как эта структура данных будет реализована как OO-объект, поэтому я сосредоточусь вместо ее реализации на Haskell. Вот мой первый разрез:

    -- file:  c:/HaskellDev/eventProcessor/eventProcessor.hs

    type Timestamp = Integer
    type ClassName = String
    type LineNumber = Integer
    type Message = String
    type Key = String
    type Value = String
    type Property = (Key, Value)
    type Properties = [Property]

    data Event= Event Timestamp ClassName LineNumber Message Properties
      deriving (Show)

Я определил свойства как список структур свойств, где свойство представляет собой 2-кортеж, состоящий из строкового ключа и строкового значения. Надеемся, что эта структура будет достаточно интересной, чтобы предоставить нетривиальный пример обработчика событий в Haskell. Обратите внимание, что я включил получение (Показать) в определение типа, чтобы ghci знал, как вывести значение моего нового типа.

Теперь, когда мы определили тип данных, давайте создадим значение Event:

    *Main> let prop1 = ("sessionId", "ABCD1234")
    *Main> :type prop1
    prop1 :: ([Char], [Char])
    *Main> let props = [prop1]
    *Main> let evt1 = Event 1320512200548 "java.lang.String" 1293 "NPE in substring()" props
    *Main> evt1
    Event 1320512200548 "java.lang.String" 1293 "NPE in substring()" [("sessionId","ABCD1234")]

Далее мы рассмотрим, что мы можем захотеть сделать с такими данными, и как мы можем их обработать в Haskell.

Генерация данных

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

В этом примере я собираюсь сосредоточиться на случае постобработки. Одна из причин заключается в том, что я пока не хочу вдаваться в программирование на Haskell, управляемое событиями, а вторая причина в том, что я могу пропустить (на данный момент) не-FP-чистый код, используемый для получения Events ( файловый ввод / вывод, например) и сосредоточиться только на коде «чистый FP». Поэтому я предполагаю, что у нас есть готовый список значений в Haskell для обработки в нашем примере. Я установлю данные в своем файле eventProcessor.hs, введя столько значений, сколько смогу, прежде чем скука будет преодолена. Я добавлю два свойства в каждый список свойств: идентификатор сеанса и идентификатор пользователя (при условии, что сеансы приходят и уходят для одного и того же пользователя, и у пользователя может даже быть открыто несколько сеансов одновременно).В моем примере данных будет три пользовательских сеанса одновременно. Обратите внимание, что присвоение переменной let in является артефактом ghci, поэтому нет необходимости, если вы делаете свои назначения в исходном файле Haskell, под моим определением типа данных Event:

 prop1 = ("sessionId", "ABCD1234")
    prop2 = ("sessionId", "EFGH5678")
    prop3 = ("sessionId", "WXYZ9876")
    prop4 = ("userId", "smith")
    prop5 = ("userId", "adams")
    prop6 = ("userId", "jobim")

    propList1 = [prop4, prop1]
    propList2 = [prop5, prop2]
    propList3 = [prop6, prop3]

    eventList = [
      Event 1320512200548 "java.lang.String" 1293 "NPE in substring()" propList1,
      Event 1320512200699 "javax.swing.JPanel" 388 "initialized" propList3,
      Event 1320512203699 "javax.swing.JList" 1255 "model replaced" propList3,
      Event 1320513130333 "com.adamsresearch.jarview.JarView" 388 "fileNotFound" propList2,
      Event 1320513255342 "com.adamsresearch.jarview.FileFilter" 79 "initialized" propList1,
      Event 1320513257324 "com.adamsresearch.jarview.ArchiveSearch" 193 "search started13255342" propList2,
      Event 1320512259333 "javax.lang.Integer" 133 "number format exception" propList3,
      Event 1320512260122 "com.adamsresearch.jarview.JarView" 725 "search started" propList2,
      Event 1320512263122 "com.adamsresearch.jarview.JarView" 779 "search completed" propList2,
      Event 1320512265147 "javax.swing.JPanel" 388 "initialized" propList1
      ]

Обратите внимание на распространенную ошибку новичка, которую я сделал изначально — я знаю, что отступы важны в Haskell, но я все же допустил ошибку отступа старого стиля, заключив заключительную, заключительную квадратную скобку в столбец 1. Это приводит к ошибке, потому что Закрывающая скобка сама является частью оператора присваивания List. Отсюда и отступ, расположенный выше, в последней строке назначения eventList. Отступ означает, что мы все еще в операторе присваивания.

Обработка данных

Учитывая этот список примеров данных, при программировании в императивном стиле вы, вероятно, прибегнете к циклическому оператору для обработки отдельных событий. У Haskell нет конструкции цикла; вместо этого, научитесь думать о циклической обработке списка как

  • Рекурсивно работает с каждым элементом списка или
  • Использование библиотечных функций Haskell, которые работают со всеми элементами списка.

Хотя мы остановимся на втором случае, в большинстве книг по FP сначала обсуждается первый случай, поскольку он кажется более естественным переходом от императивного программирования, поэтому мы также будем придерживаться этого подхода.

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

-- file:  c:/HaskellDev/eventProcessor/clone.hs

    clone :: [a] -> [a]

    clone (x:xs) = x : clone xs
    clone [] = []

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

Чтобы увидеть эту функцию в действии, мы загружаем ее в ghci, затем проверяем значение List, возвращаемое, когда мы передаем List для клонирования:

*Main> :load clone.hs
    [1 of 1] Compiling Main             ( clone.hs, interpreted )
    Ok, modules loaded: Main.
    *Main> let clone1 = clone [1,2,3,4,5]
    *Main> clone1
    [1,2,3,4,5]

Теперь перейдем к чему-то нетривиальному: я хочу извлечь из наших примеров данных все значения Event с определенным идентификатором пользователя. В клоне мы взяли каждое значение без разбора, потому что хотели их всех; в этом случае нам нужно будет сопоставить шаблон с атрибутами события. Не только это, но мы должны найти идентификатор пользователя в списке свойств, вложенных в событие. Эти требования позволят мне показать пример с чуть большим содержанием, чем пример «Hello, World».

Напомним формат нашего мероприятия:

Event 1320512200548 "java.lang.String" 1293 "NPE in substring()" [("userId","smith"),("sessionId","ABCD1234")]

Допустим, мы заинтересованы в извлечении каждого события с указанным идентификатором пользователя, и давайте вызовем функцию getMessagesForUser. Для каждого события, как только мы определяем, что идентификатор пользователя совпадает, мы добавляем itemit к списку, который будет возвращаемым значением функции.

Способ, которым я подошел к этому, состоит в том, чтобы признать, что мне сначала нужна функция, которая будет перебирать список пар ключ-значение, ища ключ userId и значение (фактически, второй элемент 2-кортежа), соответствующее переданному в идентификатор пользователя. Я назову эту функцию matchUserId.

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

    timestamp (Event ts _ _ _ _) = ts

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

data Property = Property {
      key :: Key,
      value:: Value }
      deriving (Show)

    data Event= Event {
      timestamp :: Timestamp,
      className :: ClassName,
      lineNumber :: LineNumber,
      message :: Message,
      properties :: Properties }
      deriving (Show)

Сделав это, мне нужно изменить свою инициализацию значений свойств, так как теперь они больше не являются простыми 2-кортежами:

    prop1 = Property "sessionId" "ABCD1234"
    prop2 = Property "sessionId" "EFGH5678"
    prop3 = Property "sessionId" "WXYZ9876"
    prop4 = Property "userId" "smith"
    prop5 = Property "userId" "adams"
    prop6 = Property "userId" "jobim"

Помните, что мой файл eventProcessor.hs создает список образцов объектов Event. Теперь я могу использовать функцию head для получения первого события в списке, а затем использовать новую функцию доступа к метке времени, чтобы извлечь метку времени, как показано ниже. Также обратите внимание, что подпись типа метки времени («принимает событие; возвращает метку времени») может отображаться в ghci:

 *Main> let evt1 = head eventList
    *Main> evt1
    Event {timestamp = 1320512200548, className = "java.lang.String", lineNumber = 1293, message = "NPE in substring()", properties = [("userId","smith"),("sessionId","ABCD1234")]}
    *Main> :type timestamp
    timestamp :: Event -> Timestamp
    *Main> timestamp evt1
    1320512200548

Теперь мы можем вернуться к нашей функции matchUserId. Я решил, что перед созданием этой функции мне нужна дополнительная небольшая функция, которая просматривает одно свойство и возвращает значение True, если ключ является userId, а значением является переданный идентификатор пользователя. Я вызвал эту функцию isUserId и поместил ее в файл eventProcessor.hs, прямо под определением типа данных Event:

 isUserWithId :: Property -> String -> Bool
    isUserWithId prop id =
      if (key prop == "userId" && value prop == id)
        then True
        else False

Обратите внимание на отступ. Я определил сигнатуру типа так, что нам нужно будет обсудить позже, когда мы поговорим о частичных функциях и карри. Достаточно сказать, что функция принимает Property и String и возвращает Bool. Мы можем протестировать эту функцию с некоторыми свойствами, которые я создаю в этом файле:

 *Main> prop1
    Property {key = "sessionId", value = "ABCD1234"}
    *Main> isUserWithId prop1 "smith"
    False
    *Main> prop4
    Property {key = "userId", value = "smith"}
    *Main> isUserWithId prop4 "smith"
    True
    *Main> prop6
    Property {key = "userId", value = "jobim"}
    *Main> isUserWithId prop6 "smith"
    False

Завершив эту функцию, мы можем работать с containsUserPropertyWithValue, которое ищет идентификатор пользователя в списке свойств. Вот что я придумал:

    containsUserPropertyWithValue :: [Property] -> String -> Bool
    containsUserPropertyWithValue (x:xs) id =
      if (isUserWithId x id)
        then True
        else containsUserPropertyWithValue xs id
    containsUserPropertyWithValue [] _ = False 

и мы можем проверить это по паре списков свойств, которые мы создали в наших образцах данных:

*Main> containsUserPropertyWithValue propList3 "jobim"
    True
    *Main> containsUserPropertyWithValue propList3 "smith"
    False

Прежде чем мы продолжим, обратите внимание, что в Haskell есть предложение where, в котором я могу добавить свою функцию isUserWithId непосредственно к этой функции. Это имеет смысл в этом случае; Мне вряд ли понадобится эта функция где-либо еще, кроме функции containsUserPropertyWithValue. Давайте попробуем это:

  containsUserPropertyWithValue :: [Property] -> String -> Bool
    containsUserPropertyWithValue (x:xs) id =
      if (isUserWithId x id)
        then True
        else containsUserPropertyWithValue xs id
      where
        isUserWithId prop id =
          if (key prop == "userId" && value prop == id)
            then True
            else False
    containsUserPropertyWithValue [] _ = False

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

*Main> isUserWithId prop4 "smith"

    :1:1: Not in scope: `isUserWithId'

Эта функция была определена только в другой функции и поэтому больше не доступна для меня как отдельная функция.

Последний шаг — использовать эту функцию при переборе всего списка событий. Как я описал ранее, мы не будем повторяться в обычном смысле. Сначала я поэкспериментирую с использованием рекурсивной функции. При каждом вызове функция добавляет событие в список, если оно соответствует указанному идентификатору пользователя. Вот как выглядит мой первый срез:

listEventsForUser :: [Event] -> String -> [Event]
    listEventsForUser (x:xs) id =
      if (containsUserPropertyWithValue (properties x) id)
        then x : listEventsForUser xs id
        else listEventsForUser xs id
    listEventsForUser [] _ = []

Если я выполню этот пример для набора событий, сгенерированных в этом файле, я получу именно то, что ожидал

Prelude> :load eventProcessor.hs
    [1 of 1] Compiling Main             ( eventProcessor.hs, interpreted )
    Ok, modules loaded: Main.
    *Main> let evtList1 = listEventsForUser eventList "adams"
    *Main> evtList1
    [Event {timestamp = 1320513130333, className = "com.adamsresearch.jarview.JarView", lineNumber = 388, message = "fileNotFound",
properties = [Property {key = "userId", value = "adams"},Property {key = "sessionId", value = "EFGH5678"}]},
    Event {timestamp = 1320513257324, className = "com.adamsresearch.jarview.ArchiveSearch", lineNumber = 193, message = "search started13255342",
properties = [Property {key = "userId", value = "adams"},Property {key = "sessionId", value = "EFGH5678"}]},
    Event {timestamp = 1320512260122, className = "com.adamsresearch.jarview.JarView", lineNumber = 725, message = "search started",
properties = [Property {key = "userId", value = "adams"},Property {key = "sessionId", value = "EFGH5678"}]},
    Event {timestamp = 1320512263122, className = "com.adamsresearch.jarview.JarView", lineNumber = 779, message = "search completed",
properties = [Property {key = "userId", value = "adams"},Property {key = "sessionId", value = "EFGH5678"}]}]
    *Main>

(с небольшим форматированием вывода, чтобы было легче увидеть, что были выбраны правильные события).

Эта статья стала длиной в главу (хотя обратите внимание, насколько компактен фактический рабочий код!), Но у меня есть еще одна тема для обсуждения. Языки FP, такие как Haskell, распознают описанный выше шаблон так часто, что в языке имеется встроенная поддержка для выполнения операций над каждым элементом в Списке, что приводит к чрезвычайно компактному, но очень читабельному коду. Например, есть функция отображения Haskell, которая принимает функцию в качестве аргумента и применяет эту функцию к каждому элементу в списке. Некоторые такие функции возвращают списки, а другие уменьшают списки до скалярного значения. В нашем случае было бы полезно использовать функцию фильтра, которая применяет функцию к каждому элементу списка и возвращает список элементов, для которых фильтр имеет значение true.

Вот как я бы использовал фильтр в этом примере. Во-первых, я хотел бы определить функцию, которая будет применяться ко всему событию. Чтобы сделать вещи немного более компактными, я включу (во вложенных выражениях where) мои ранее определенные функции следующим образом:

eventGeneratedByUser :: String -> Event -> Bool
    eventGeneratedByUser id event =
      if (containsUserPropertyWithValue (properties event) id)
        then True
        else False
      where
        containsUserPropertyWithValue (x:xs) id =
          if (isUserWithId x id)
            then True
            else containsUserPropertyWithValue xs id
          where
            isUserWithId prop id =
              if (key prop == "userId" && value prop == id)
                then True
                else False
        containsUserPropertyWithValue [] _ = False

Это хорошее время для «тизера» по частичным функциям. Обратите внимание, как я определил свою сигнатуру функции — сначала я поставил идентификатор пользователя String, а затем Event. Причиной этой организации является то, что я хочу использовать фильтр Haskell, который принимает предикат. В большинстве примеров предикат представляет собой нечто простое, например нечетную функцию Haskell, которая не требует аргументов. Если вы скажете filter odd [1,2,3,4,5,6], вы получите [1,3,5].

Что вы делаете, если вам нужно передать аргумент функции (в данном случае, ID пользователя) для создания предиката? Ответ заключается в этой странной подписи, в этом случае


eventGeneratedByUser :: String -> Event -> Bool

Я передаю идентификатор пользователя и событие. Именно в этом порядке. Вы, наверное, уже заметили, что подпись (определяю ли я ее или запрашиваю у ghci)
не

    eventGeneratedByUser :: String  Event -> Bool

как и следовало ожидать от функции, принимающей два аргумента. Функция выглядит (из фактической подписи) как последовательность вызовов функций с одним аргументом. Причина: все функции в Haskell на самом деле принимают только один аргумент! Кто-то может исправить мое описание, но я смотрю на это так: эта сигнатура определяет функцию, которая принимает строку, и эта вновь определенная функция сама является функцией, которая принимает событие. В моем случае это очень помогает, потому что в Haskell вы можете взять функцию с несколькими аргументами и создать
частичную функцию, для которой вы определили
некоторые, но не все аргументы. Отсюда и выбор моего порядка моих аргументов. Этот порядок позволяет мне создать предикат, который является частичной функцией, в которой «идентификатор пользователя» уже определен:

    *Main> let filterPred = eventGeneratedByUser "adams"

Поскольку я не полностью указал аргументы этой функции, я получаю функцию, которая требует только Event для возврата True или False. Теперь у меня есть предикат, который мне нужен для фильтра, и вот как я его использую:

 *Main> let eventsForAdams = filter filterPred eventList
    *Main> eventsForAdams
    [Event {timestamp = 1320513130333, className = "com.adamsresearch.jarview.JarView", lineNumber = 388, 
message = "fileNotFound", properties = [Property {key = "userId", value = "adams"},Property {key = "sessionId", value = "EFGH5678"}]}, Event {timestamp = 1320513257324, className = "com.adamsresearch.jarview.ArchiveSearch", lineNumber = 193,
message = "search started13255342", properties = [Property {key = "userId", value = "adams"},Property {key = "sessionId", value = "EFGH5678"}]}, Event {timestamp = 1320512260122, className = "com.adamsresearch.jarview.JarView", lineNumber = 725,
message = "search started", properties = [Property {key = "userId", value = "adams"},Property {key = "sessionId", value = "EFGH5678"}]}, Event {timestamp = 1320512263122, className = "com.adamsresearch.jarview.JarView", lineNumber = 779,
message = "search completed", properties = [Property {key = "userId", value = "adams"},Property {key = "sessionId", value = "EFGH5678"}]}]

Если бы я не смог определить частичную функцию в Haskell, я бы не использовал функцию фильтра Haskell для этого упражнения, так как она принимает один аргумент — предикат. Этот предикат (теперь функция с одним аргументом ищет событие) применяется к каждому элементу списка событий, и я получаю именно то, что хотел.

Мне очень понравилось создавать этот пост. Для тех из вас, кто находится на том же уровне понимания ФП, что и я, я надеюсь, что вы нашли этот пост интересным и полезным.

 

От http://wayne-adams.blogspot.com/2011/11/haskell-from-oo-developers-perspective.html