Функциональное программирование на Haskell: Часть 3. Определение функций

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

Алексей Бешенов, технический писатель, независимый специалист

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



25.02.2010

Соглашения по именованию

Для начала подытожим использующиеся в Haskell соглашения по именованию.

  • Язык чувствителен к регистру, т. е. идентификаторы foobar, fooBar и FooBar различаются.
  • Имена функций, аргументов и ти́повых переменных должны начинаться со строчной буквы. Далее могут следовать 0 и более букв любого регистра, цифр, подчеркиваний и штрихов '.
  • Имена типов и классов должны начинаться с заглавной буквы.
  • Зарезервирован ряд ключевых слов, которые не могут использоваться в качестве идентификаторов: case, class, data, default, deriving, do, else, if, import, in, infix, infixl, infixr, instance, let, module, newtype, of, then, type, where, _.

Подробности см. в Haskell 98 Report.

Некоторые неформальные соглашения:

  • Принято использовать так называемый camelCase, когда слова в идентификаторе пишутся слитно, но каждое новое слово начинается с заглавной буквы.
  • Как и в математике, часто используются имена со штрихом на конце: x, x', x'', …
  • На конце имен списков часто указывается s, как при образовании множественного числа в английском языке: xs, ys, zs, …
  • Для ти́повых переменных принято использовать короткие имена, чаще однобуквенные.

Комментарии и грамотное программирование

В обычном файле .hs многострочные комментарии располагаются между символами {- и -}, а однострочные – после --. Многострочные комментарии могут быть вложенными.

… -- комментарий

{-
  …
  {-
    комментарий
  -}
  …
-}

Дональд Кнут предложил «грамотное программирование» («literate programming»), в котором текст программы сопровождается подробным и последовательным объяснением логики работы на естественном языке. При этом код в первую очередь предназначается для чтения человеком, и лишь затем – для выполнения компьютером.

Если вместо расширения .hs использовать .lhs, то будет использоваться «грамотный» подход, и всё содержимое по умолчанию будет считаться комментариями, а код необходимо помечать отдельно: либо начальным символом >, либо блоками между \begin{code} и \end{code}:

Комментарии

> код
> …
> …

Комментарии

\begin{code}
код
…
…
\end{code}

При компиляции комментарии и метки комментариев будут игнорироваться. Вариант с \begin{code} и \end{code} предназначается для набора текстов в системе LaTeX. Подробности см. по адресу http://haskell.org/haskellwiki/Literate_programming

К этим статьям мы прилагаем «грамотный» исходный код.


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

if-then-else

Условные конструкции в Haskell могут записываться различным образом. Среди прочего, имеется стандартная конструкция:

if условие then выражение1 else выражение2

Условие должно иметь тип Bool. Если его значение – True, то результатом будет выражение после then, иначе – выражение после else.

Пример:

abs x = if x >= 0 then x else -x

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

Ветвь else обязательна. Как следствие, любое выражение с if-then-else разбирается однозначно (иначе возникали бы ситуации, когда не ясно, к какому открывающему if относится закрывающее else).

Уравнения с условиями

Часто условий и ветвей несколько. Как, например, в случае [signum.png]

Тогда можно использовать уравнения с условиями:

signum x | x >  0  =  1
         | x == 0  =  0
         | x <  0  = -1

Условия стоят после знака | и до знака =. Они просматриваются по порядку: если значение первого – True, то результатом считается следствие после знака =; если значение – False, то проверяется второе условие, и т. д.

При этом условия могут пересекаться и даже не покрывать в совокупности все возможные ситуации. Если все условия будут просмотрены и ни одно не даст True, мы получим ошибку времени выполнения.

Часто последним условием специально указывается такое, которое всегда равно True. Для удобства в Prelude определено

otherwise = True

(по-английски otherwise означает «иначе»).

abs x | x >= 0     =  x
      | otherwise  = -x

Сопоставление с образцом

Многие функции естественно определяются через разбор различных случаев. Например, булевы функции:

not :: Bool -&gt; Bool
not False  =  True
not True   =  False

(&&) :: Bool -&gt; Bool -&gt; Bool
False && False  =  False
False && True   =  False
True  && False  =  False
True  && True   =  True

(||) :: Bool -&gt; Bool -&gt; Bool
False || False  =  False
False || True   =  True
True  || False  =  True
True  || True   =  True

Образцами называются выражения в левой части уравнений, с которыми сопоставляются фактические аргументы функций. Сначала происходит сопоставление с образцом в первом уравнении, в случае удачного сопоставления используется правая часть; в противном случае происходит сопоставление с образцом во втором уравнении и т. д.

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

Если какое-либо значение не используется в правой части, то в образце можно указать _. Например, False && _ = False, True || _ = True. Поэтому наши определения можно упростить:

(&&) :: Bool -&gt; Bool -&gt; Bool
False && _  =  False
True  && x  =  x

(||) :: Bool -&gt; Bool -&gt; Bool
False || x  =  x
True  || _  =  True

Такие записи лучше, потому что при сопоставлении с _ в образце значение не играет роли и не вычисляется. Для && второй аргумент будет вычислен только в том случае, если первый аргумент – True; для || – только если первый аргумент – False.

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

bot :: a
bot = error "Bottom!"

Для первого варианта определений без _:

True  && ⊥  =  ⊥
False && ⊥  =  ⊥

True  || ⊥  =  ⊥
False || ⊥  =  ⊥
Для упрощенного варианта:
True  && ⊥  =  ⊥
False && ⊥  =  False

True  || ⊥  =  True
False || ⊥  =  ⊥

Таким образом, вид образцов и порядок следования уравнений влияет на вычисления. В Prelude (||) и (&&) определены в соответствии со вторым вариантом.

Образец может представлять собой список или кортеж. Исходя из этого, можно записать такие определения:

fst :: (a,b) -&gt; a
fst (x,_)  =  x

snd :: (a,b) -&gt; b
snd (_,y)  =  y

head :: [a] -&gt; a
head (x:_) = x

tail :: [a] -&gt; [a]
tail (_:xs) = xs

Чтобы разобраться с определениями head и tail, достаточно вспомнить, что список конструируется при помощи :. Скобки вокруг образцов вроде (x:xs) обязательно должны указываться, так как применение функции имеет самый высокий приоритет. В противном случае f x:xs будет интерпретироваться как (f x):xs.

Что произойдет, если с приведенными определениями попробовать вычислить head [] или tail []?

Мы получим ошибку:

ghci&gt; head []
*** Exception: Non-exhaustive patterns in function head

[] не соответствует образцу x:_, а других уравнений для head нет. Эта ситуация нежелательна; если для некоторых аргументов функция не вычисляется, то лучше явно добавить значение undefined, либо понятные сообщения об ошибках:

head []  =  error "empty list"

tail []  =  error "empty list"

В этом случае мы получим более ясное сообщение «Exception: empty list».

Иногда удобно дать имя целому образцу, чтобы использовать в правой части уравнения. Например, рассмотрим определение функции dropWhile из Prelude, которая принимает некоторый предикат (т. е. функцию типа a -&gt; Bool) и список и вычисляет список, который получится, если отбросить начальные элементы, которые удовлетворяют предикату. Например,

ghci&gt; dropWhile (&gt;0) [1,2,-23,4,5]
[-23,4,5]

Определение может быть таким:

dropWhile :: (a -&gt; Bool) -&gt; [a] -&gt; [a]
dropWhile _ [] =  []
dropWhile p (x:xs') | p x       = dropWhile p xs'
                    | otherwise = (x:xs')

Образцу (x:xs') можно присвоить свое имя и переписать второе уравнение:

dropWhile p xs@(x:xs') | p x       = dropWhile p xs'
                       | otherwise = xs

xs@(x:xs') – это @-образец (по-английски @ читается как «as»). Он всегда успешно привязывает имя xs к выражению, сопоставляемому с (x:xs').

case

Для сопоставления с образцом можно использовать case-конструкции.

s x = case x of
        1 -> 4
        2 -> 3
        3 -> 2
        4 -> 1
        _ -> 0

После case x of следует цепочка образцов-посылок и следствий, отделяемых знаком ->. x будет последовательно сопоставляться с образцами, пока не будет найден подходящий.

Следствия должны иметь одинаковый тип.

Последним случаем по умолчанию может быть _ -> …, так как заполнитель _ успешно сопоставляется с любым значением.

Остальные определения можно переписать с использованием case. В этом смысле case можно считать базовой конструкцией.

-- Запись нескольких уравнений через case
x || y = case (x,y) of
           (False,x) -> x
           (True, _) -> True
-- Запись if-then-else через case
abs x = case x >= 0 of
          True  ->  x
          False -> -x

let и where

Если требуется создать локальные привязки, то используется let:

roots a b c =
  let d  = b^2 - 4*a*c
      sd = sqrt d
      x1 = (-b + sd)/(2*a)
      x2 = (-b - sd)/(2*a)
  in (x1, x2)

После let следует блок с объявлениями, затем in и выражение, использующее объявления.

Можно также использовать образцы и объявления функций. Объявления вычисляются рекурсивно, поэтому одно может выражаться через другое.

Конструкции let могут быть вложенными.

Кроме того, есть конструкция where, которая работает схожим образом:

roots a b c = (x1, x2) where
  d  = b^2 - 4*a*c
  sd = sqrt d
  x1 = (-b + sd)/(2*a)
  x2 = (-b - sd)/(2*a)

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

f x y z  |  p1 x y z  =  …
         |  p2 x y z  =  …
         |   . . . . . . .
         |  pn x y z  =  …
       where
         …

Выравнивание кода

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

Группировка определений требуется после ключевых слов do, let, of, where. (do в действии мы увидим позже; остальные конструкции нам уже знакомы.) Мы уже применяли правильное выравнивание – посмотрите приведенные выше определения с case-of, where и let.

Длинные выражения можно переносить; главное чтобы перенесенная часть начиналась после той колонки, в которой начато определение:

where
  foo = a + b + c + d
          + e + f + g + h
              + i + j + k + l

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

s x = case x of {1 -> 4; 2 -> 3; 3 -> 2; 4 -> 1; _ -> 0}

roots a b c = (x1, x2) where {d = b^2-4*a*c; sd = sqrt d; x1 
		= (-b+sd)/(2*a); x2 = (-b-sd)/(2*a)}

Однако на практике достаточно использовать выравнивание, и код с правильным выравниванием легче читать.

Если что-то не так, то ghci выдаст сообщение: «Parse error (possibly incorrect indentation)».


Специальные конструкции для списков

Диапазоны

Чтобы можно было удобно записывать выражения списков, в Haskell имеются диапазоны. Их элементы должны принадлежать классу Enum.

Используются следующие методы (в предыдущей статье мы их опустили, чтобы упростить изложение):

enumFrom :: a -> [a]
enumFromThen :: a -> a -> [a]
enumFromTo :: a -> a -> [a]
enumFromThenTo :: a -> a -> a -> [a]

Базовый синтаксис таков: [x..y] вычисляется в список

[x, succ x, (succ.succ) x, (succ.succ.succ) x, …, y]

Пример:

ghci> ['a'..'c']
"abc"
ghci> [1..23]
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]

(Значение ['a'..'c'] записано в виде "abc", но это сокращение для ['a','b','c'].)

Чтобы перечисление происходило с другим шагом или в обратном порядке, используется запись [x,x'..y], где x' – элемент, который должен следовать за первым элементом x, а y – последний элемент.

ghci> [1,3..23]
[1,3,5,7,9,11,13,15,17,19,21,23]
ghci> ['z','y'..'a']
"zyxwvutsrqponmlkjihgfedcba"

Диапазоны могут соответствовать бесконечным спискам. В ленивом языке это не создает проблем.

ghci> head [0..]
0
ghci> take 5 [0..]
[0,1,2,3,4]
ghci> [0..] !! 23
23

Выделение

В аксиоматике теории множеств Цермело–Френкеля имеется аксиома выделения, позволяющая построить множество на основе имеющегося, выбрав элементы, которые соответствуют определенному предикату.

По аналогии имеются специальные выражения для выделения списков. В простейшем виде это записывается так:

[e1 | n <- e2]

Здесь e1 и e2 – произвольные выражения, где e2 вычисляется в список. Выражение n <- e2 называется генератором. n будет пробегать все значения из списка e2 и использоваться в e1.

ghci> [n^2 | n <- [1..10]]
[1,4,9,16,25,36,49,64,81,100]

Генераторов может быть несколько, они перечисляются через запятую:

ghci> [(m,n) | m <- [1..3], n <- [1..3]]
[(1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(3,1),(3,2),(3,3)]

Перестановка генераторов дает тот же список, но с другим порядком элементов:

ghci> [(m,n) | n <- [1..3], m <- [1..3]]
[(1,1),(2,1),(3,1),(1,2),(2,2),(3,2),(1,3),(2,3),(3,3)]

Это можно сравнить со вложенными циклами. Первый генератор – внешний цикл, а второй – внутренний. В соответствии с этим последующие генераторы могут зависеть от предыдущих. Например, рассмотрим функцию

concat' xs = [y | ys <- xs, y <- ys]

Если вычислить concat' [[1,2,3],[4,5,6],[7,8,9]], то ys будет пробегать значения [1,2,3], [4,5,6], [7,8,9], а y будет пробегать значения каждого из этих списков. Таким образом, concat' работает как уже известная нам функция concat.

Собственно выделение задействуется, когда мы добавляем условия. Например, рассмотрим список троек (x,y,z), таких, что x2 + y2 = z2 и 1 ≤ x,y,z ≤ 15. Все возможные тройки мы получим при помощи трех генераторов x <- [1..15], y <- [1..15], z <- [1..15]. Условие x^2+y^2==z^2 добавляется после них:

ghci> [(x,y,z) | x <- [1..15], y <- [1..15], z <- [1..15], x^2+y^2==z^2]
[(3,4,5),(4,3,5),(5,12,13),(6,8,10),(8,6,10),(9,12,15),(12,5,13),(12,9,15)]

Следующая функция будет вычислять список делителей числа (включая 1, но исключая само число):

factor n = [m | m <- [1..n `div` 2], mod n m == 0]
ghci> factor 100
[1,2,4,5,10,20,25,50]
ghci> factor 23
[1]

Теперь можно составить при помощи factor список чисел, равных сумме своих множителей (такие числа называются совершенными):

ghci> [n | n <- [1..], n == (sum.factor) n]
[6,28,496,8128,…

Если число делится только на 1 и на себя, то оно называется простым. Можно написать соответствующий предикат:

isPrime n = factor n == [1]
ghci> isPrime 100
False
ghci> isPrime 23
True

При этом не забывайте, что вычисления ленивые и, если хвост списка factor n – не [], то сравнение с [1] даст False. Полное вычисление factor n не требуется.

Отсюда получим список всех простых чисел:

ghci> [n | n <- [1..], isPrime n]
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,…

(Более красивое решение мы скоро получим при помощи решета Эратосфена.)


Первые примеры

Отображение списков

Теперь мы покажем несколько популярных примеров функциональных программ.

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

Рассмотрим простую практическую задачу. Пусть у нас имеется список элементов [a, b, c, …] и требуется заменить каждый элемент на [f a, f b, f c, …].

Эта операция называется отображением (map).

map f [a, b, c, …] = [f a, f b, f c, …]

Для начала можно записать тип необходимой функции:

map :: (a -> b) -> [a] -> [b]

В функциональном языке принято мыслить рекурсивно, и списки сами по себе рекурсивны. Желаемые манипуляции со списком требуется описать относительно головы и хвоста:

  • Головной элемент x должен быть заменен на f x.
  • Все элементы в хвосте xs должны быть отображены, т. е. xs заменяется на map f xs.

Базой рекурсии служит пустой список. Здесь, очевидно, действует тривиальное правило:

map _ [] = []

Шаг рекурсии будет выражать уравнение

map f (x:xs) = f x : map f xs

Пример вычисления:

map (*2) [1,2,3] = map (*2) (1:2:3:[])
                 = (*2) 1 : map (*2) (2:3:[])
                 = 2 : map (*2) (2:3:[])
                 = 2 : (*2) 2 : map (*2) (3:[])
                 = 2 : 4 : map (*2) (3:[])
                 = 2 : 4 : (*2) 3 : map []
                 = 2 : 4 : 6 : map []
                 = 2 : 4 : 6 : []
                 = [2,4,6]

Как только мы фиксируем f, map превращается в конкретное отображение. Например, можно определить

doubleElements = map (*2)
negateElements = map negate

(Вспомните о каррировании и частичном применении.)

Другие примеры использования:

ghci> map (*2) [1,2,3]
[2,4,6]
ghci> map (\ x -> 3*x+2) [1,2,3]
[5,8,11]
ghci> map (>0) [1,2,3]
[True,True,True]

Написанную нами функцию можно найти в Prelude.

Это типичная для функциональных языков абстрактная функция высокого порядка. Вместо того чтобы записывать конкретное отображение, мы используем произвольную функцию f, которая становится параметром map.

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

Решето Эратосфена

Древнегреческому математику Эратосфену Киренскому приписывается следующий алгоритм нахождения простых чисел:

  1. Сначала выписываются все целые числа больше 2 (1 простым числом не считается).
  2. Из списка выбирается первое простое число p (на первом шаге p = 2), и вычеркиваются числа 2p, 3p, 4p, …
  3. Далее берется следующее невычеркнутое число p, и процесс повторяется.
  4. Все оставшиеся в списке числа будут простыми.

Пример просеивания [2..100] через решето Эратосфена приведен на рисунке 1.

Рисунок 1. Пример просеивания [2..100] через решето Эратосфена
Рисунок 1. Пример просеивания [2..100] через решето Эратосфена

Список всех целых чисел больше 2 можно записать в виде [2..].

Вычеркивание чисел, кратных p, можно записать в виде

elim :: Integer -> [Integer] -> [Integer]
elim p xs = [x | x <- xs, mod x p /= 0]

Запишем рекурсивную функцию sieve («решето»). Она будет принимать список (p:xs), первый элемент (голова) которого p должен быть простым. Далее она вычеркивает p из хвоста xs и снова просеивает результат через решето:

sieve :: [Integer] -> [Integer]
sieve (p:xs) = p : sieve (elim p xs)

Теперь всё готово. Наш бесконечный список [2..] можно просеять через решето, и получится бесконечный список простых чисел:

primeNumbers :: [Integer]
primeNumbers = sieve [2..]

Например, вычислим первые 23 простых числа:

ghci> take 23 primeNumbers
[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83]

Если вы попробуете вычислить primeNumbers, то список будет печататься бесконечно. Элементы будут выводиться по мере вычисления.


Быстрая сортировка

Очень часто возникает задача сортировки. Исходные данные – некоторый список [x1, …, xn]. Требуется найти список [x'1, …, x'n], состоящий из тех же элементов, такой, что x'1 ≤ … ≤ x'n.

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

quickSort :: (Ord a) => [a] -> [a]

Алгоритм быстрой сортировки определяется рекурсивно. Каждый шаг выглядит так:

  • Выбрать ведущий элемент списка y.
  • Разбить оставшиеся элементы на xs, которые меньше ведущего или равны ему, и на zs, которые больше.
  • Вставить перед y отсортированные xs, а после – отсортированные zs.

Рекурсия заключается в том, что xs и zs сортируются тем же алгоритмом.

Как всегда, рекурсивное определение сводит задачу к более простой, а именно сортировке меньших списков. Базовым случаем можно считать сортировку пустого списка:

quickSort [] = []

Если список не пустой, то в качестве элемента y удобно взять его голову. Тогда xs и zs набираются из хвоста. Через выделение списков это запишется так:

xs = [x | x <- ys, x<=y]
zs = [z | z <- ys, z>y]

Соединение списков запишется в виде xs ++ [y] ++ zs.

Итак, основным уравнением будет

quickSort (y:ys) = xs ++ [y] ++ zs where
  xs = quickSort [x | x<-ys, x<=y]
  zs = quickSort [z | z<-ys, z>y]

Всё готово. На рисунке 2 приведен пример вычисления quickSort [3,2,5,3,1,2,4]. Рекурсивные вызовы показаны в виде дерева, а конкатенация списков – в виде пошаговой свертки ветвей.

Рисунок 2. Пример вычисления quickSort [3,2,5,3,1,2,4]
Рисунок 2. Пример вычисления quickSort [3,2,5,3,1,2,4]

Заключение

Мы рассмотрели важные элементы Haskell, которые используются при определении функций, а также специальные выражения для построения списков.

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

В общем случае, при функциональном подходе:

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

В качестве упражнения по определению функций и применению рекурсии вы можете записать свои аналоги для drop, length, null, take, zip (их описание содержится в предыдущей статье).

По аналогии с описанной в тексте dropWhile попробуйте определить takeWhile, которая будет вычислять начальный список элементов, удовлетворяющих данному предикату.

Запишите другие полезные функции для работы со списками:

iterate f x = [x, f x, (f.f) x, (f.f.f) x, …]  (бесконечный список)
repeat x = [x,x,x,x,...]  (бесконечный список)
cycle [x1,…,xn] = [x1,…,xn, x1,…,xn, x1,…,xn, x1,…,xn, …]  (n >= 1)
replicate n x = [x,x,x,x,…]  (n элементов)

Вы можете свериться с прилагаемым к статье файлом.

В следующей статье мы рассмотрим работу с алгебраическими типами данных.

Прилагаемые файлы:fph03.lhs, fph03ex.lhs

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


  • Bluemix

    Узнайте больше информации о платформе IBM Bluemix, создавайте приложения, используя готовые решения!

  • developerWorks Premium

    Эксклюзивные инструменты для построения вашего приложения. Узнать больше.

  • Библиотека документов

    Более трех тысяч статей, обзоров, руководств и других полезных материалов.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=469801
ArticleTitle=Функциональное программирование на Haskell: Часть 3. Определение функций
publish-date=02252010