Статьи

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


Сегодня я смотрю на определение типа Haskell и использование сопоставления с образцом в функциях.
Сопоставление с образцом — гораздо более неотъемлемая черта FP, чем OO. Но сначала …

Определение типа Haskell

Согласно
HaskellWikiалгебраический тип данных — это тип, который определяет форму элементов. Один из способов подойти к этой теме, если вы склонны думать в ОО, — это 1) думать о структурах C и 2) попытаться на мгновение забыть о поведении. Даже в ОО мы все были там — иногда вы действительно хотите что-то вроде структуры просто для организации некоторых данных, и единственное поведение (в смысле ОО, означающее «методы»), которое вам действительно нужно, — это набор методов доступа. / мутаторы (геттеры / сеттеры). Таким образом, вы либо с трудом сглатываете и делаете атрибуты общедоступными, либо вы взрываете шаблонные методы получения / установки. Между тем, некоторые люди пишут рамки кода, которые автоматически выставляют ваши атрибуты и так далее.

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

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

type Label = String
type FileName = String
type LineNumber = Int
type MethodName = String
type ExceptionName = String
type Description = String

data Breakpoint = LineNumberBreakpoint Label FileName LineNumber Description |
MethodBreakpoint Label FileName MethodName Description |
ExceptionBreakpoint Label ExceptionName Description
deriving (Show)

Это не самый простой пример, но он представляет полезную структуру и позволяет мне представить несколько вещей одновременно. Сначала сфокусируйтесь на ключевом слове данных; Данные — это начало определения алгебраического типа данных, за которым следует имя типа (которое должно начинаться с заглавной буквы). За этим именем типа следует знак равенства и определение конструктора типа. «получение (показ)» не имеет решающего значения для понимания типа; он предоставляет информацию, необходимую ghci для вывода значений, которые мы создадим на основе этого определения типа.

Определение конструктора типа состоит из одного или нескольких конструкторов значений. Эти конструкторы значений также должны быть помечены именем, начинающимся с заглавной буквы, и разделены символом «|» символ. Конструкторы значений и конструкторы типов находятся, так сказать, в разных «пространствах имен», поэтому, если у вас действительно простой тип данных, вы можете использовать одну и ту же метку как для имени определения типа, так и для имени конструктора значения и не беспокоиться о конфликтах между два имени. Например:

type User = User String String String Int

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

data Breakpoint = Breakpoint String String Int String

или я мог бы улучшить читаемость / понятность определения с помощью некоторых синонимов типа:

type Label = String
type FileName = String
type LineNumber = Int
type Description = String

data Breakpoint = Breakpoint Label FileName LineNumber Description

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

Возвращаясь к нашему примеру, следующий вопрос будет «что вы делаете с этим определением типа?» На данный момент я хочу запустить интерпретатор / отладчик Глазго, ghci. Как только все будет готово, я могу загрузить свой файл. Обратите внимание, что ghci загружается по умолчанию из каталога, в котором он был запущен; Вы можете установить каталог, из которого он загружается: cd
dirName :

*Main> :cd c:/HaskellDev
*Main> :load typesDemo.hs
[1 of 1] Compiling Main             ( typesDemo.hs, interpreted )
Ok, modules loaded: Main.
*Main>

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

*Main> :type ExceptionBreakpoint
ExceptionBreakpoint
:: Label -> ExceptionName -> Description -> Breakpoint

Объяснение цепочки символов -> придется подождать до позднего дня, так как оно немного чуждо уму OO. Но сейчас мы можем использовать вывод, чтобы напомнить себе, что нам нужно предоставить конструктору значений. Давайте создадим ExceptionBreakpoint, выведем его значение, а затем используем ghci’s: type для вывода его типа:

*Main> let bp3 = ExceptionBreakpoint "NPE" "java.lang.NullPointerException" "Break on all NullPointerExceptions"
*Main> bp3
ExceptionBreakpoint "NPE" "java.lang.NullPointerException" "Break on all NullPointerExceptions"
*Main> :type bp3
bp3 :: Breakpoint

Обратите внимание: тип возвращает фактический тип точки останова, а не имя конструктора значения, который использовался для его построения. Бывают моменты, когда мы хотим узнать, какой конструктор значений был использован — отличный переход ко второй теме этого поста.

Сопоставление с образцом в функциях

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

describeBreakpoint (ExceptionBreakpoint _ _ desc) = desc

Я определил функцию — descriptionBreakpoint — которая соответствует только на ExceptionBreakpoint. Я написал это, чтобы соответствовать на кортеже, который состоит из

  • имя конструктора значения
  • две подстановочные знаки («_», заполнители) для значений, которые в данный момент меня не интересуют (Label и ExceptionName)
  • параметр для описания, desc.

После знака равенства следует значение, возвращаемое функцией, которая является просто описанием. Применяя эту функцию к точке останова, которую мы создали ранее, я получаю:

     *Main> describeBreakpoint bp3
    "Break on all NullPointerExceptions"

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

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

   describeBreakpoint (LineNumberBreakpoint _ _ _ desc) = desc
  describeBreakpoint (MethodBreakpoint _ _ _ desc) = desc
  describeBreakpoint (ExceptionBreakpoint _ _ desc) = desc

Если бы мы этого не сделали, мы бы получили ошибку, если бы попытались использовать функцию для описания LineNumberBreakpoint:

 *Main> let bp4 = LineNumberBreakpoint "parseParam" "Parser.java" 79 "Break at beginning of parse"
   *Main> describeBreakpoint bp4
   "*** Exception: typesDemo.hs:15:1-56: Non-exhaustive patterns in function describeBreakpoint

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

*Main> :load typesDemo.hs
[1 of 1] Compiling Main             ( typesDemo.hs, interpreted )
Ok, modules loaded: Main.
*Main> let bp3 = ExceptionBreakpoint "NPE" "java.lang.NullPointerException" "Break on all NullPointerExceptions"
*Main> let bp4 = LineNumberBreakpoint "parseParam" "Parser.java" 79 "Break at beginning of parse"
*Main> let bp5 = MethodBreakpoint "initBean" "Parser.java" "init()" "Break at parser init() method"
*Main> describeBreakpoint bp3
"Break on all NullPointerExceptions"
*Main> describeBreakpoint bp4
"Break at beginning of parse"
*Main> describeBreakpoint bp5
"Break at parser init() method"

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

     secondElement (a, b, c, d) = b

а затем попробуйте это:

*Main> secondElement ("Washington", "Lincoln", "Fillmore", "Roosevelt")
"Lincoln"

Замечательно. Теперь посмотрим на это:

*Main> secondElement ("Washington", "Lincoln", "Fillmore")

:1:15:
   Couldn't match expected type `(t0, t10, t20, t30)'
               with actual type `(t1, t2, t3)'
   In the first argument of `secondElement', namely
     `("Washington", "Lincoln", "Fillmore")'
   In the expression:
     secondElement ("Washington", "Lincoln", "Fillmore")
   In an equation for `it':
       it = secondElement ("Washington", "Lincoln", "Fillmore")

Хотя
в этом списке
есть второй элемент, он является 3-кортежем, и в функции secondElement нет шаблона, который соответствует 3-кортежу. Мы могли бы добавить еще одно совпадение в определение функции для обработки 3-х кортежей (и т. Д.), Но есть более эффективные способы сделать это, которые мы можем исследовать в следующих постах.

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

 

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