Конспекты лекций «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] и изымает из этого списка первый элемент.
«_» — означает, что данный элемент нас не интересует.

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

Подробнее
Реклама
Комментарии 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
                
                • да, об этом (определения 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
                      Такое ощущение складывается, что Хаскель очень хорошо подошел бы как интерфейс какой-нибудь специализированной 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х элементов
                            [] — для пустого списка

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