Пользователь
0,0
рейтинг
28 ноября 2013 в 21:48

Разработка → Конспекты лекций «Haskell как первый язык программирования». Часть1 tutorial

image
Привет Habr! Сегодня я достал свои старые конспекты по курсу «Haskell как первый язык программирования» Сергея Михайловича Абрамова и попробую максимально доходчиво и с примерами рассказать об этом замечательном языке тем, кто с ним еще не знаком. Рассказ ориентирован на неподготовленного читателя. Так что, даже если вы впервые услышали слово Haskell…

Вторая часть

Базовые типы Haskell

Базовые типы языка Haskell — это:
Числа
Логические величины
Символы
Списки
Упорядоченные множества (tuples)
Функции

Числа
Целые:
Integer (-∞,∞)
Int (-2^31, 2^31-1)
В прелюдии (стандартной библиотеке) определенно много полезных функций для целых чисел, в том числе, и преобразование в число с плавающей точкой (fromInt и fromInteger)

Числа с плавающей точкой:
Float (7 знаков после запятой)
Double (16 знаков после запятой)

Логические величины
Bool (True | False)
Операции конъюнкции, дизъюнкции и отрицания (&&, ||, not)

Символы
Char (’a’)
И функции Char в Int и Int в Char (ord, chr)

Списки
Списки могут быть разные:
[Int] — список целых чисел [1,2,3,4]
[Char] — список символов (строка)
[[Int]] — массив
[Float -> Float] — это список функций
и т. д.

Несколько стандартных операций в примерах:
Main> head [1,2,3]
1
Main> tail [1,2,3]
[2,3]
Main> length [True,False]
2
Main> reverse [1,2,3]
[3,2,1]
Main> 0:[1,2,3]
[0,1,2,3]
Main> — строка приглашения в консоли компилятора ghci
«:» — операция присоединения элемента к списку.

Упорядоченные множества
Примеры:
(2.4, ”cat”) (Float, [Char])
(’a’, True, 1) (Char, Bool, Int)
([1,2],sqrt) ([Int], Float->Float)
(1, (2, 3)) (Int, (Int, Int))

Но, сердце Haskell и всего функционального программирования — это, конечно, сами функции!

Функции

Функция, в современной математике, это закон соответствия, который сопоставляет каждому элементу x из данного множества A один единственный (или ни одного) элемент y из множества B.
Haskell, по своему назначению, это, прежде всего, язык математиков, поэтому синтаксис тут максимально точно соответствует этому определению.
Пример:
square :: Integer -> Integer
square x = x*x

Как вы, наверное, догадались, это функция возведения числа в квадрат. Разберем её подробно:

Первая строка — это объявление функции:
Имя_функции :: область_определения — > область _значений
square :: Integer -> Integer
Тут следует сказать, что в Haskell совсем необязательно всегда объявлять функцию. В ряде случаев интерпретатор и так поймет какие у данной функции области определения и значения. Однако, опускать объявления — моветон.

Вторая строка — это определение функции:
Имя_функции параметры = правило_вычисления
square x = x*x

Функция без параметров есть ничто иное, как константа:
e :: Float
e = exp 1.0


Функция с несколькими параметрами:
Спасибо janatem за разъяснения (читай комментарии).
abcFormula :: Float -> Float -> Float -> [Float]
abcFormula a b c = [
	(-b+sqrt(b*b-4.0*a*c))/(2.0*a),
	(-b-sqrt(b*b-4.0*a*c))/(2.0*a)
	]
-- находит корни уравнения ax^2+bx+c=0


Определения функций с альтернативами

Как и в любом языке, в Haskell есть конструкции ветвления.
Разберем их на примере функции abs (модуль).
If … then … else …
abs1 x = if x>=0 then x else -x


Case … of …
abs2 x = case x>=0 of
	   True  -> x
	   False -> -x


Но, помимо стандартных if и case, в Haskell есть очень красивая и наиболее используемая конструкция ветвления. Так называемые, охранные выражения. Пример:
abs3  x	| x>0 = x
		| x<0 = -x
		| otherwise = 0 


Прямую черту следует читать, как: «при».
Читаем: «Функция abs3, с входным параметром x, при x>0 принимает значение x, при x<0 принимает значение -x, и в любом другом случае принимает значение 0».
Конечно, мы могли записать все с помощью двух охранных выражений, но я записал три, что бы было понятно, что их может быть сколько угодно.
Otherwise в прелюдии определен очень просто:
otherwise        :: Bool
otherwise        =  True

То есть, можно спокойно написать вместо «otherwise» «True», но это, опять же, моветон.

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

Один из наиболее распространенных и эффективных приемов в Haskell — это сопоставление с образцом. Вместо параметра мы можем подсунуть функции пример того, как должен выглядеть параметр. Если образец подошел функция выполняется, если нет — переходит к следующему образцу. Например, определение факториала через рекурсию с помощью образцов:
fact :: Integer -> Integer 
fact 0 = 1
fact n = n * fact (n-1)

Тоже самое, но, с помощью охранных выражений:
fact :: Integer -> Integer
fact n   | n==0 = 1
         | n>0  = n*fact (n-1)  

Есть очень распространенный образец для списка: (x:xs). X — обозначает первый элемент, XS — остальной список (кроме первого элемента). «:» — операция присоединения элемента к списку. Примеры из прелюдии:
head :: [a] -> a
head (x:_) =  x
head [] =  error "Prelude.head: empty list"

tail :: [a] -> [a]
tail (_:xs) =  xs
tail [] =  error "Prelude.tail: empty list"

Функция head принимает на вход список чего угодно [a] и возвращает первый элемент этого списка. Функция tail принимает на вход список чего угодно [a] и изымает из этого списка первый элемент.
«_» — означает, что данный элемент нас не интересует.

Ну вот, на сегодня и все. Если будет интерес, в ближайшее время напишу продолжение.
Сергий Смирнов @serr
карма
18,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (38)

  • +9
    В этой статье используется очень прагматичный подход к ознакомлению, которые не затрагивает ряд догматичных аспектов языка.

    Например, говорится, что к базовым типам относятся Числа, Логические величины, Символы, Списки, Упорядоченные множества (tuples) и Функции. По какому признаку одни типы относятся к базовым, а другие — нет? Возможные видимые варианты ответа:

    1. как языковая элементарная конструкция, без которой язык ущербен? С самого начала, даже в официальном описании языка, подчёркивается, что многие типы из Prelude могут быть описаны натуральными средствами языка, например, data Bool = True | False, или data Char = 'a' | 'b' | ... Многие типы из приведённых допускают альтернативное описание. С другой же стороны, IO не может быть описан средствами языка, хотя в списке базовых типов не значится.

    2. как реализуемые особым образом типы? Например, список — участок памяти, Bool — всем привычный bool, Int — всем привычный 4-байтный int. Если так, то почему сюда не включен IO? Да и зачем в языке столь высокого уровня с первых шагов акцентировать внимание на такой конкретике.

    3. как типы сорта * в противопоставление типам сорта * -> * или более сложных сортов? Да, Int :: *, Char :: *, Bool :: *, но [] :: * -> * уже не подходит.
    Так почему именно эти типы были названы базовыми?

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

    Я не знаю, на кого рассчитана статья. И несмотря на это, хорошо, что появляются новые туториалы по языку. Продолжение не будет лишним, но мне кажется, что лучше собрать уже имеющуюся русскоязычную литературу и опираться на неё во избежание повторения уже написанного и изложенного, но ради дополнения.
    • +2
      Попытался скомпоновать так, что бы было понятно тем, кто знаком с императивными языками.
      Некоторые темы не затрагивал специально, что бы не выпадать из темы.
      Список базовых типов был взят из лекции «как есть» и столь глубокий анализ я, увы, не проводил.
      При все этом, признаю замечания справедливыми. В дальнейшем постараюсь исправить.
    • 0
      Дык вот же
      newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
      
      • 0
        Для этого в языке требуются «уникальные типы», как в Clean и Mercurry. Без них программист сможет создать новый мир не разрушив старого.
        • 0
          Каким образом, если конструктор RealWorld не экспортируется?
          • 0
            Разработчик библиотеки до него сможет добраться.
            • 0
              Какой библиотеки? base?
              • 0
                Да. Он же тоже человек, нельзя ему такое позволять.
                • 0
                  Тогда надо FFI запрещать, там и не такое можно сделать.
            • 0
              не всё так страшно:

              {-
              This is a generated file (generated by genprimopcode).
              It is not code to actually be used. Its only purpose is to be
              consumed by haddock.
              -}
              
              -- | @State\#@ is the primitive, unlifted type of states.  It has
              -- 	one type parameter, thus @State\# RealWorld@, or @State\# s@,
              -- 	where s is a type variable. The only purpose of the type parameter
              -- 	is to keep different state threads separate.  It is represented by
              -- 	nothing at all. 
              
              data State# s
              
              -- | @RealWorld@ is deeply magical.  It is /primitive/, but it is not
              -- 	/unlifted/ (hence @ptrArg@).  We never manipulate values of type
              -- 	@RealWorld@; it\'s only used in the type system, to parameterise @State\#@. 
              
              data RealWorld
              
    • НЛО прилетело и опубликовало эту надпись здесь
      • +1
        Потому что путаете types и kinds.
        Prelude> :k []
        [] :: * -> *
        
        • НЛО прилетело и опубликовало эту надпись здесь
          • +5
            Kinds имеют такое же отношение к types, как types к values. То есть можно считать их типами типов, если это не слишком запутывает.

            Разговор про kinds, и все на самом деле достаточно просто. Int и Double имеют kind *, что означает, что они являются типами данных сами по себе. А вот просто списка быть не может — может быть список из Int, список из Double, или список из списков из Double, и т. д., то есть он параметризуется другим типом, поэтому его kind — это * -> *. А вот функция (->) тоже не может быть сама по себе, она из некого типа в другой, поэтому она параметризуется двумя типами, так что ее kind — это * -> * -> *.

            Интереснее то, что kind есть не только у типов, но и, например, классов типов. Функтор, например, параметризуется неким типов, kind которого * -> *, и мы получаем некий тип данных, но не совсем любой — про него известно, как минимум, что для него определна операция fmap, что в некотором смысле является ограничением (constraint), так что
            Prelude> :k Functor
            Functor :: (* -> *) -> Constraint
            
            • +4
              … как первый язык, говорите…
              • 0
                В любом приличном языке есть сложные вещи. К счастью, все их знать не обязательно — при начальном обучении их можно опустить.
                • 0
                  Начальное обучение предполагает знакомство с базовым вводом-выводом, который в хаскеле предполагает понимание монад, что с трудом может быть отнесено к базовым навыкам.
                  • 0
                    Понимание монад для ввода-вывода не обязательно. Надо понимать, что операция ввода-вывода — first-order value и просто так не выполняется.
            • 0
              Не знал, что kinds применим к классам типов.
              Только на многопараметрические классы странно себя ведут. Есть у меня
              class Tr t v | t -> v where
                getVal :: t -> v
                getForest :: t -> [t]
              

              :k на него выдает
              *Main> :k Tr
              Tr :: * -> * -> Constraint
              

              А я ждал что-то типа
               Tr :: (*,*) -> Constraint
              • 0
                Перепутал — скобочки в :k Functor не заметил.
                Заметил — все стало логичным.
    • 0
      На счёт основных типов (1, 2).
      Булевый тип — не базовый.
      Он определён как
      data Bool = False | True
      


      Список же определён как
      data [a] = [] | a : [a]
      

      То есть обычным способом. Необычность заключается ЛИШЬ в особом синтаксисе типа:
      Поскольку по правилам языка его надо было определить так:
      data [] a = [] | a :  ([] a)
      

      И к тому же, символы '[', ']' — запрещены к использованию в именах конструкторов.
      А в памяти он находится как обычные данные.

      Кортежи(туплы) так же используют повторы запятой как конструктора — и только этим они особенные.
      А вот числа и символы — да, реализованы отдельно (примитивами языка)

      IO — не базовый тип, есть тип IO a — он да, самим языком не описывается. Но ничего особенного в плане размещения в памяти нет.

      (3) На счёт сортов, тупл — это многомерный сорт.
      (,,,) :: * -> * -> * -> * -> *
      (Int,Double) :: *
      Maybe :: * -> *
      Maybe Int :: *
      Так что сортность — не показатель базовости.

      Базовость — то, что есть в Прелюдии.
      Хотя там есть и
      data Ordering = LT | EQ | GT
      
      • 0
        да, об этом (определения Bool, [], кортежей) тоже стоит упомянуть.
        Если под базовостью понимать вхождение в Прелюдию, то стоит упомянуть о Maybe, Ordering и др. Впрочем, автор согласился, что у него только поверхностно описываются «базовые» типы, чем я удовлетворён.

        К слову, сказано, что любой тип данных и функция из Прелюдии может быть описана в терминах языка (как Bool или Maybe) или псевдокода (как списки или кортежи) и реализована непредопределённым образом для каждой реализации компилятора, допуская как реализацию напрямую, как написано, так и по собственному разумению.
        То есть тот набор типов, которые реализованы отдельно (по Вашим словам) некими примитивами, не определён стандартом, а определяется каждой конкретной реализацией. Этот набор может не ограничиваться Int и Char.
        • 0
          Да, можно попытаться сделать что-то самому, пользуясь не совсем каноническими функциями, такими как:
          unsafeCoerce :: a -> b
          
          unsafePerformIO :: IO a -> a
          
          unsafeIOToST :: IO a -> ST s a
          
          unsafeSTToIO :: ST s a -> IO a
          


          А можно попросить девелоперов и использовать расширения языка и прагмы.
          Например, использовать не-ленивые типы(Bang patterns) и даже не-коробочные типы (unboxed types).
    • 0
      Для обычного человеку (не математика и не программиста) изучение языка это тренировка порождения и распознавания его конструкций. Как языка программирования, так и естественного. На начальном этапе строгость менее важна, чем возможность попробовать.
  • +1
    Функция с несколькими параметрами

    Не стоит вводить в заблуждение начинающих изучать язык. В Хаскеле (и в некоторых других (всех?) функциональных языках высшего порядка) все функции имеют ровно один аргумент; говорить, что их несколько — жаргонизм.

    Существует два способа имитировать полезную программистам множественность аргументов.
    (1) Объявить функцию, возвращающую в качестве результата другую функцию
    f :: a -> b -> c, что эквивалентно f :: a -> (b -> c).

    (2) Использовать кортеж (tuple) в качестве аргумента:
    f :: (a, b) -> c. Здесь (a, b) — это не группа параметров, как в языках наподобие Си, а самодостаточный тип, который можно использовать где угодно, в том числе в качестве возвращаемого результата функции.

    • 0
      Спасибо. Сделал в статье отсылку к Вашему комментарию.
  • +1
    Может стоит начать с того для каких задач полезен/удобен этот язык?
    • +1
      Можете посмотреть, в каких категориях больше пакетов, и делать выводы: hackage.haskell.org/packages/
      • 0
        там лидируют Data — 790 (что логично) и Web — 529 :)))
    • 0
      Мне тоже интересен этот вопрос. Какое у этого языка преимущество или недостатки по сравнению с другими языками программирования?
      • 0
        Вы становитесь функциональной элиткой и можете тешить свое ЧСВ

        habrahabr.ru/post/190492/
        • +8
          У нас это называлось лямбда-самец.
          • +2
            freakin' made my day )))
  • 0
    Такое ощущение складывается, что Хаскель очень хорошо подошел бы как интерфейс какой-нибудь специализированной NoSQL. С его «ленивыми вычислениями» можно было бы творить чудеса в обработке сложных структур данных (например тот же анализ статистических данных). Или я неправ?
    • 0
      Прав. Как минимум, на Хаскеле достаточно широко используются NoSQL-решения: acid-state.seize.it/
  • 0
    Я правильно понимаю, что (x:xs) определено для массива из одного элемента и не определено для пустого массива?
    • 0
      Абсолютно верно.
    • +1
      x : xs — определено для массива 1 и более элементов,
      x : y : xs — для 2х и более
      x : y : z : xs — для 3х и более
      [x] — для одного элемента
      [x, y] — для 2х элементов
      [x, y, z] — для 3х элементов
      [] — для пустого списка

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.