Статьи

Подробнее об организации функций на Haskell / FP; ленивая оценка

После того, как мой последний пост прокрутился внизу страницы, я понял, что упустил пару возможностей: одна связана с дополнительной оптимизацией кода, а другая — с темой ленивой ( или нестрогой) оценки .

Во-первых, позвольте мне рассмотреть, что я делал. Я обрабатывал структуру данных, которую использовал для представления событий, генерируемых монитором процесса. Структура включает метку времени, имя класса и номер строки источника, текстовое сообщение и набор свойств. Отображение одного из них в ghci дает следующее:

Event {timestamp = 1320513130333, className = "com.adamsresearch.jarview.JarView",
 lineNumber = 388, message = "fileNotFound", properties = 
[Property {key = "userId", value = "adams"},Property {key = "sessionId", value = "EFGH5678"}]}

Моя цель состояла в том, чтобы написать — в аутентичном стиле FP — некоторый код для извлечения из Списка этих событий событий, значение свойства userId которых соответствует определенному идентификатору пользователя. Вот код (без определения типа и т. Д.), Который я в итоге использовал:

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

Наконец, в ghci я получил список нужных мне событий со следующим:

    *Main> let filterPred = eventGeneratedByUser "adams"
    *Main> let eventsForAdams = filter filterPred eventList

Одна из тем, о которых объектно-ориентированные разработчики постоянно слышат в FP, — это перестать мыслить циклично и начать думать с точки зрения рекурсии. Дополнительная тема (насколько я могу это выразить), чтобы избежать рекурсии, когда вы можете использовать функцию в стиле отображения, то есть функцию, которая структурно определена для работы со всеми элементами списка. Когда вы настраиваете рекурсивную функцию для обработки списка, большая часть того, что вы создаете, является шаблонной. Boilerplate — это проклятие разработчика: некоторые с этим согласны (кто-нибудь хочет разобраться в шаблоне для Java GridBagLayout и GridBagConstraints?), В то время как другие заменяют его «каркасами», которые остальные обычно находят даже менее привлекательными, чем написание стандартного кода. сами.

Как видно из приведенного выше исходного кода, я использовал рекурсию для создания логической функции, которая определяет, связано ли событие с пользователем. Однако, когда я фактически отфильтровал список, вместо использования рекурсивной функции я использовал фильтр Хаскелла, передав ему свою частичную функцию. На этом последнем этапе я чувствую, что наконец-то написал в духе Haskell и FP в целом, но теперь давайте еще раз посмотрим на этот исходный код и посмотрим, есть ли место для улучшений.

Сначала обратите внимание на мою внутреннюю вложенную функцию isUserWithId:

isUserWithId prop id =
 if (key prop == "userId" && value prop == id)
   then True
   else False

Обратите внимание, что из моего старого поста я использовал синтаксис записи Haskell для определения типа данных Property и что Haskell использовал вывод типа, чтобы распознать, что я передаю структуру Property в функцию isUserWithId. Все, что делает эта функция, это просматривает пару ключ-значение и возвращает True, если ключ userId и значение соответствует параметру, который я передаю. Я думаю, что эта функция в порядке «как есть».

Включающая функция, найденная в предложении where функции eventGeneratedByUser, является рекурсивной функцией. Давайте повторим всю функцию:

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

То, что я делаю с этой функцией, по сути итерирует элементы Property события, возвращая True, как только я нахожу свойство, соответствующее требуемому условию.

Может ли это быть преобразовано из рекурсивной функции в нечто более компактное? Как оказалось, да, легко. Haskell содержит функцию с именем any, которая действует в List и возвращает True, если какой-либо элемент List имеет значение True. Чтобы использовать эту функцию в моем примере, я бы перестроил свой код для вычисления значения isUserWithId для каждого свойства в структуре данных, обернул эти логические значения в список и применил любую функцию Haskell к списку. Если вам интересно, буду ли я тратить впустую циклы, вычисляя значения, которые мне могут не понадобиться, потерпите меня несколько минут.

Первый вопрос заключается в том, как создать список логических значений, результаты применения isUserWithId к списку свойств. Для начала позвольте мне преобразовать isUserWithId в отдельную функцию, но прежде чем мы это сделаем, давайте сделаем то же самое, что мы делали в моем последнем посте — планируем заранее, чтобы иметь возможность использовать его как частичную функцию. Под этим я подразумеваю, что мы хотим применить эту функцию к списку значений свойств, где «идентификатор пользователя» будет встроен, так сказать, в определение функции. Я переопределю функцию, чтобы сначала взять идентификатор пользователя, чтобы позже я мог создать частичную функцию, которую я могу применить непосредственно к свойству (обратите внимание, что это также включает в себя изменение порядка аргументов в уравнении isUserWithId):

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

Мы можем проверить функцию как в «полной форме», где мы передаем и идентификатор пользователя, так и свойство, и в ее частичной форме, где мы сначала создаем частичную функцию с указанным идентификатором пользователя, а затем передаем только Имущество:

    *Main> prop4
    Property {key = "userId", value = "smith"}
    *Main> isUserWithId "smith" prop4
    True
    *Main> let containsSmith = isUserWithId "smith"
    *Main> containsSmith prop4
    True

Далее я возьму из своего примера три коротких списка свойств и объединю их в большой список:

    *Main> let bigPropList = concat [propList1,propList2,propList3]
    *Main> bigPropList
    [Property {key = "userId", value = "smith"},Property {key = "sessionId", value = "ABCD1234"},
Property {key = "userId", value = "adams"},
Property {key = "sessionId", value = "EFGH5678"},Property {key = "userId", value = "jobim"},Property {key = "sessionId", value = "WXYZ9876"}]

Теперь давайте использовать any для возврата True, если какой-либо элемент в списке соответствует нашей частичной функции, которая проверяет, есть ли у свойства идентификатор пользователя «smith»:

    *Main> any containsSmith bigPropList
    True

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

Чтобы интегрировать этот код в нашу новую версию eventGeneratedByUser, мы могли бы сделать следующее:

eventGeneratedByUser :: String -> Event -> Bool
eventGeneratedByUser id event =
 any (isUserWithId id) (properties event)

а затем проверьте это в ghci:

    *Main> head eventList
    Event {timestamp = 1320512200548, className = "java.lang.String", lineNumber = 1293, message = "NPE in substring()", properties = [Property {key = "userId", value = "smith"},Property {key = "sessionId", value = "ABCD1234"}]}
    *Main> eventGeneratedByUser "smith" (head eventList)
    True

Обратите внимание, что код значительно более компактен и (в отличие от некоторых языков, где «terse» = «плотный») фактически более читабелен. На самом деле все настолько просто, что мы можем пойти на шаг дальше, чем мы делали раньше, и предоставить функцию для возврата подмножества List, что мы только что сделали интерактивно в ghci в последний раз:

getEventsGeneratedByUser :: String -> [Event] -> [Event]
getEventsGeneratedByUser id events =
 filter (eventGeneratedByUser id) events

и, тестирование в ghci:

    *Main> getEventsGeneratedByUser "jobim" eventList
    [Event {timestamp = 1320512200699, className = "javax.swing.JPanel", lineNumber = 388, message = "initialized", 
properties = [Property {key = "userId", value = "jobim"},Property {key = "sessionId", value = "WXYZ9876"}]},Event
{timestamp = 1320512203699, className = "javax.swing.JList", lineNumber = 1255, message = "model replaced", properties =
[Property {key = "userId", value = "jobim"},Property {key = "sessionId", value = "WXYZ9876"}]},Event {timestamp = 1320512259333,
className = "javax.lang.Integer", lineNumber = 133, message = "number format exception", properties = [Property {key = "userId",
value = "jobim"},Property {key = "sessionId", value = "WXYZ9876"}]}]

Обратите внимание, насколько компактен и удобен для чтения наш результирующий код. У нас есть одна функция, которая определяет, содержит ли Property идентификатор пользователя с указанным значением — 5 строк кода, включая объявление типа. У нас есть две дополнительные функции, одна из которых возвращает True, если событие генерируется указанным пользователем, а другая возвращает подмножество списка событий, удовлетворяющих этому условию — каждые 3 строки кода, включая объявление типа. И, как я упоминал ранее, в данном случае компактный фактический означает читабельность.

Ленивая оценка

В приведенном выше коде вы могли бы задаться вопросом: «Какое влияние на производительность оказывает циклический просмотр списка, скажем, 5000 свойств и возвращение True, если только одно из них соответствует тесту? Особенно, если первое свойство соответствует тесту?». Haskell оценивает выражения только тогда, когда они необходимы. Технически в Haskell это называется
нестрогой оценкой , и она позволяет вам делать некоторые вещи, которые действительно выглядят странно в Java. Например, в Haskell вы можете определить список как диапазон элементов, используя ..; следующее определяет список целочисленных значений от 1 до 9:

    *Main> [1..9]
    [1,2,3,4,5,6,7,8,9]

Хотя это может выглядеть нормально, что вы думаете об этом?

    Prelude> [1..]
    [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40
,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60
,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80
,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100
,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120
,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135
    ...

где я добавил разрывы строк, чтобы было легче читать. Большой сюрприз: это бесконечный список, начинающийся с целого числа 1, и ghci с радостью продолжит выводить этот вывод на экран навсегда или до тех пор, пока не произойдет что-то действительно плохое, в зависимости от того, что произойдет раньше. Мне пришлось нажать Control-C, чтобы остановить вывод. Но вы можете присвоить этот бесконечный список переменной:

    Prelude> let infiniteList = [1..]
    Prelude>

без проблем. Причина в том, что infiniteList еще не был оценен (заполнен). Если бы вы попытались показать это в ghci, вы бы снова вернулись к бесконечному выводу. Неограниченная оценка позволяет нам делать некоторые интересные вещи. Например, вы можете взять первые 11 элементов бесконечного списка:

    Prelude> take 11 infiniteList
    [1,2,3,4,5,6,7,8,9,10,11]
    Prelude>

Эта функция позволяет нам писать код, который (например) возвращает True, если «любой» элемент в Списке удовлетворяет некоторому предикату. Haskell не собирается просеивать весь список, оценивая каждое выражение, а затем шагать по списку и применять предикат. Так что не имеет значения, сколько свойств имеют мои события; Я верну Истину, как только получу совпадение. Обратите внимание, что есть случаи, когда все элементы действительно будут оцениваться; например, если вы суммируете элементы списка (конечно, вы не ожидали, что Haskell даст вам сумму элементов бесконечного списка, верно?).

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

 

От http://wayne-adams.blogspot.com/2011/11/more-on-haskellfp-function-organization.html