LINUX.ORG.RU

[scheme][haskell][oop][fp] Мысли вслух

 , ,


5

7

Была на ЛОРе такая тема — [Haskell] простой вопрос. Хотелось бы немножко её развить и высказаться на тему предпочтения того или иного языка при изучении ФП (графомания mode on :)).

У Scheme есть довольно давняя история использования в качестве подопытного языка в курсах изучения ФП. Я не знаю чем это вызвано, но факт остаётся фактом — есть известный курс у MIT (или был?) и разные полезные книжки — SICP, HTDP, PLAI, OOPLAI, которые обычно и советуют читать если нужно ознакомиться с ФП.

Касательно SICP — одним из сторонников использования ML для целей изучения ФП была написана статья (http://www.cs.kent.ac.uk/people/staff/dat/miranda/wadler87.pdf) в которой достаточно хорошо освещены некоторые недостатки Scheme. Если говорить про Haskell, то тут всё так же. Далее, по пунктам (опуская кое-что из того что уже было в той статье).

Более явный синтаксис

Вместо

(define (foo x y z)
  (if (> (+ x (* y z) 1) 7) (print (+ x y)) (print (- x y))))

будет

foo x y z = if x + y * z + 1 > 7 then print $ x + y else print $ x - y

при этом по-прежнему можно написать выражение в префиксной форме:

(if' ((>) ((+) x ((*) y z) 1) 7) (print ((+) x y)) (print ((-) x y)))

почти как в Scheme. То есть, кроме префикса также есть (расширяемый пользователем) инфикс (в том числе функции вроде ($) и (.) позволяющие в некоторых случаях опускать лишние аргументы у функций и некоторые скобки в выражениях) и разные специальные формы (вроде if, let, лямбды и т.п.). Во всём что не касается макросов это более удобно. S-выражения обретают свой особый смысл только когда доходит до их цитирования:

'(if (> (+ x (* y z) 1) 7) (print (+ x y)) (print (- x y)))

и разбора с целью написания макросов. Тем не менее, для изучения именно ФП эта возможность незначительна (ФП не про макросы, в SICP и HTDP не ни слова про макросы, в PLAI есть только немного, в OOPLAI — побольше). Про то как правильно (ну, _правильно_, то есть без использования s-выражений) организовывать символьные вычисления (вроде дифференцирования из SICP) также расказывается в упомянутой статье.

Каррированные функции

Такое определение, например:

(define add
  (lambda (n)
    (lambda (m)
      (+ m n))))

заменяется простым

add = (+)

так как все функции уже каррированные (позволяют частичное применение). Фактически, в хаскеле функция с n аргументами сразу задаёт n разных функций (выбор конкретной функции осуществляется во время компиляции и не имеет эффекта во время выполнения). Некаррированные функции это функции от кортежей (те и другие переводятся друг в друга с помощью ФВП carry/uncarry).

Частичное применение, секции, pointfree запись

add2 = (+ 2)

add2 5
7

вместо

(define add2 (add 2))

(add2 5)
7

Мутабельные замыкания

Это единственная вещь которая есть в Scheme и которую можно не увидеть сразу в хаскеле (и про неё нет в той статье). Тот тред был как раз про них. Чтобы прояснить этот момент, ниже приводятся некоторые примеры из OOPLAI и их аналоги на хаскеле.

Простейший вариант:

(define counter
  (let ((count 0))
    (lambda ()
      (begin
        (set! count (add1 count))
        count))))

(counter)
1
(counter)
2

аналог:

counter = (=~ (+ 1)) <$> new 0

тут (=~ (+ 1)) играет роль мутирующего «метода», а (new 0) — мутируемого «объекта», (<$>) — применение «диспетчера» (тут — просто единичный анонимный «метод»). Вся конструкция функториальная (не монадическая). Использование:

ctr <- counter      -- Инстанцирование класса counter объектом ctr.
ctr                 -- Применение единственного метода ((=~ (+ 1)) который).
1                   -- Результат.
ctr                 -- Снова.
2                   -- Другой результат.

Чуть более сложный пример:

(define counter-
  (let ((count 0))
    (lambda (cmd)
      (case cmd
        ((dec) (begin
                 (set! count (sub1 count))
                 count))
        ((inc) (begin
                 (set! count (add1 count))
                 count))))))

(counter- 'inc)
1
(counter- 'dec)
0

Для начала определим имена методов dec и inc:

data CounterMethod = Dec | Inc

это не символы и не строки (так что код не будет ill-typed как в примере на Scheme, иначе говоря, применение несуществующего метода не пройдёт компиляции). И теперь функцию:

counter' = dispatch <$> new 0
  where dispatch obj Dec = obj =~ flip (-) 1
        dispatch obj Inc = obj =~ (+ 1)

тут dispatch играет роль диспетчеризирующей функции которая получает объект (obj) и имя метода, а затем изменяет объект (как того требует метод). (new 0) — начальный объект.

Пример:

ctr <- counter'     -- Инстанцирование класса counter' объектом ctr.
ctr Inc             -- Применение метода Inc на объекте ctr.
1
ctr Inc
2
ctr Inc
3
ctr Dec             -- Тут уже метод Dec.
2
ctr Dec
1
ctr Dec
0

Тут применение (ctr Inc) весьма похоже на каноничное, через точку, obj.method и является, по сути, посылкой сообщения объекту.

Третий пример:

(define stack
  (let ((vals '()))
    (define (pop)
      (if (empty? vals)
          (error "cannot pop from an empty stack")
        (let ((val (car vals)))
          (set! vals (cdr vals))
          val)))
    (define (push val)
      (set! vals (cons val vals)))
    (define (peek)
      (if (empty? vals)
          (error "cannot peek from an empty stack")
        (car vals)))
    (lambda (cmd . args)
       (case cmd
         ((pop) (pop))
         ((push) (push (car args)))
         ((peek) (peek))
         (else (error "invalid command")))))) 

(stack 'push 1)
(stack 'push 2)
(stack 'pop)
2
(stack 'peek)
1
(stack 'pop)
1
(stack 'pop)
; cannot pop from an empty stack

аналогично:

data StackMethod = Pop | Push | Peek

stack = dispatch <$> new []
  where
    dispatch x Pop _  = get x >>= (x =~ tail >>) . return . head
    dispatch x Push y = x =~ (y :) >> return y
    dispatch x Peek _ = head <$> get x

и пример:

stk <- stack :: IO (StackMethod -> Int -> IO Int)
                    -- stack это параметрически-полиморфный класс, в данном
                    -- случае берётся его спецификация когда элементы стека
                    -- имеют тип Int (можно взять что-то более общее).
                    -- Этот специфичный класс инстанцируется объектом stk.
mapM_ (stk Push) [1, 2, 3]
                    -- (stk Push) это применение метода Push на объекте stk,
                    -- с помощью ФВП mapM_ оно производится для всех элементов
                    -- списка.
repeat 4 $ stk Pop __ >>= print
                    -- 4 раза вызывается метод Pop, элементы печатаются.
                    -- Последний раз вызывается исключение (так как стек пуст).
3
2
1
*** Exception: Prelude.head: empty list

тут точно так же — в StackMethod перечислены нужные методы для стека, функция stack определяет класс, то есть объединение данных и функций с нужным поведением, она имеет тип IO (StackMethod -> a -> IO a), то есть принимает метод, элемент стека и возвращает элемент стека (в IO, мутабельно), сама функция в IO (вся структура данных ведёт себя мутабельно).

Дальше в OOPLAI начинают использовать макросы для придания более удобоваримого вида этим конструкциям. В настоящем (ну, _настоящем_ :)) ФП этого не нужно — примитивные ООП конструкции объединяющие данные и функции выглядят естественно и так, и являются частным случаем использования ФВП, IO и ADT с паттерн-матчингом (последние два — для удобства). Использование макро-системы может иметь смысл разве что если действительно нужно реализовать сложную ООП систему (например, со множественным наследованием и изменяемой иерархией классов, впрочем, обойтись одними функциями тут тоже можно, просто придётся делать больше механических действий).

Ещё пример:

-- | Данные — конструктор и аккессоры.
data Point = Point
  { x :: Double
  , y :: Double
  } deriving ( Show, Eq ) -- ad-hoc перегруженные функции.

-- | Методы привязываемые к данным (это уже _не_ ad-hoc перегруженные функции).
data PointMethod = Pos | Mov

-- | Класс (= функция), объединяющий данные и методы.
pointClass :: Double -> Double -> IO (PointMethod -> Double -> Double -> IO Point)
pointClass initX initY = dispatch <$> new (Point initX initY)
  where
    -- | (Динамический) диспетчер по методам. Он принимает объект (Var Point),
    -- имя метода (PointMethod, т.е. статическое, в данном случае, сообщение)
    -- и два опционных аргумента для методов (Double -> Double). Эту функцию
    -- можно помещать непосредственно в Point.
    dispatch :: Var Point -> PointMethod -> Double -> Double -> IO Point
    dispatch obj Pos _ _ = get obj
    dispatch obj Mov x y = obj =: Point x y
pnt <- pointClass 2 4         -- Инстанцирование класса pointClass объектом pnt
                              -- с начальными значениями полей 2 и 4.
:t pnt
pnt :: PointMethod -> Double -> Double -> IO Point
pnt Pos __ __                 -- Вызов метода Pos на объекте pnt.
Point {x = 2.0, y = 4.0}
pnt Mov 3 5                   -- Вызов метода Mov.
Point {x = 3.0, y = 5.0}
pnt Pos __ __                 -- Положение изменилось:
{x = 3.0, y = 5.0}

Нужно заметить, что это всё довольно примитивные конструкции (простые функции и IO). В случае использования ADT для имён методов получится динамическая диспетчеризация с фиксированным набором методов (well-typed), если же переписать функцию dispatch с завязкой на хэш-табличку (которая должна быть переменной в данных класса), то будет динамическая диспетчеризация с пополняемым набором методов и перегруженными методами (одни и те же сообщения можно посылать разным инстанцированным объектам, их dispatch будет их искать в хэш-таблице и обрабатывать, это уже ill-typed, то есть с исключениями вида «нет такого метода»). Разные прочие вещи вроде наследования и self точно также можно изобразить (аггрегация данных, представление иерархии классов в данных (в переменной или нет, в зависимости от возможности менять иерархию) и более сложная функция dispatch), но как-то не интересно.

P.S.

Код на хаскеле использует такие упрощения:

import Prelude hiding ( repeat )
import Data.IORef
import Control.Applicative
import Control.Monad

type Var a = IORef a

new :: a -> IO (IORef a)
new = newIORef

get :: IORef a -> IO a
get = readIORef

(=~) :: IORef a -> (a -> a) -> IO a
x =~ f = modifyIORef x f >> get x

(=:) :: IORef a -> a -> IO a
x =: x' = x =~ const x'

repeat :: Monad m => Int -> m a -> m ()
repeat = replicateM_

__ :: a
__ = undefined

P.P.S.

OOP / ООП в контексте данного поста — объектно-ориентированное программирование в духе объединения данных и процедур, то есть в духе C++, Java, Python и т.п. _Не_ ООП в духе классы = структуры, методы = перегруженные функции, наследование = схемы агрегаций и распространения методов (как это в CLOS и классах типов Haskell).

★★★★

Ответ на: комментарий от anonymous

Взял define-syntax >> и пример >> при «выполнить» - печатает результат, я для интереса нажал на «проверить синтаксис» и тогда уже повисло (сам гуй). Может там memory limit при старте надо как-то задать?

quasimoto ★★★★
() автор топика
Ответ на: комментарий от quasimoto

DrRacket такая весёлая программа - после нажатия «проверить синтаксис» с этим кодом она у меня перестала отвечать (в консоль ничего не пишет, drracket - poll_shedule_timeout / 188MB). Может, дистропроблемы.

у меня тоже такое было (кстати, в виндовой версии проблем не было таких) Недавно обновился Racket, значительно быстрее DrRacket стал работать. Ну и хрень с проверкой синтаксиса тоже вроде починили.

qaqa ★★
()
Ответ на: комментарий от quasimoto

Хз, у меня именно гуй за два с лишним года ни разу не вис. Если бы не хватало памяти - оно бы ругнулось.

anonymous
()
Ответ на: комментарий от anonymous

То есть это мне для каждой ф-и надо куда-то лезть, вместо того, чтобы получить эту информации из кода просто один раз на него взглянув?

Это поставить курсор на функцию (должна быть объявлена где-то top-level), нажать и посмотреть её сигнатуру.

напишем мы какое-нибудь (f x) . (g x y)

вот так - f x . g x y. Ну и да, нет ничего понятнее :)

Кстати, вот что hlint думает по этому поводу:

$ echo "f x = g 1 x" > /tmp/1 ; hlint /tmp/1
/tmp/1:1:1: Error: Eta reduce
Found:
  f x = g 1 x
Why not:
  f = g 1
$ echo "map (\x -> x + 1) xs" > /tmp/1 ; hlint /tmp/1
/tmp/1:1:6: Warning: Avoid lambda
Found:
  \ x -> x + 1
Why not:
  (+ 1)
$ echo "f x = g (h x)" > /tmp/1 ; hlint /tmp/1
No suggestions

предлагать f = g . h не стал.

quasimoto ★★★★
() автор топика
Ответ на: комментарий от anonymous

Какая разница, что код на 10% длиннее, если он станет вдвое понятнее? Такое чувство, что во время написания программы стучать по клавишам - это самая сложная часть процесса :)

«Вдвое понятнее» - это какие-то фантазии. От добавления 10% мусора читабельность и понятность станет хуже. А чтение кода как раз и важнее стучания по клавишам - читать пойнтфри вообще проще, чем писать.

anonymous
()

Насколько я понял из статьи, Вадлера, он считает, что ADT и pattern-matching - преимущества ML над схемой.

Haskell добавляет тайпклассы - действительно хорошую вещь.

Да, это преимущества.

Но вот мне кажется, сначала нужно давать Scheme (пусть даже без лямбды), чтобы стало понятно, как устроен рантайм ФЯП, что такое замыкания, продолжения, итд, и показать как можно реализовать то же самое каррирование и другие вещи.

То есть, scheme - что-то вроде ассемблера.

Кстати, потом можно перейти и не к ML, а к Qi, например.

x4DA ★★★★★
()
Ответ на: комментарий от quasimoto

Это поставить курсор на функцию (должна быть объявлена где-то top-level), нажать и посмотреть её сигнатуру.

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

вот так - f x . g x y. Ну и да, нет ничего понятнее :)

[code] (define (ff y z) (f x y z)) (define (gg y) (g x y)) (lambda (x y) (ff (gg x) y)) [/code] понятнее намного

предлагать f = g . h не стал.

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

anonymous
()
Ответ на: комментарий от anonymous

От добавления 10% мусора читабельность и понятность станет хуже.

Так мы добавляем 10% необходимой для понимания кода информации, а не мусор.

читать пойнтфри вообще проще, чем писать.

Что делает эта функция: (.) . (.) (.) . (.) ?

anonymous
()
Ответ на: комментарий от anonymous

Что делает эта функция:

Служит демагогическим «аргументом», очевидно же. Кроме как в посте анонимного эксперта по скобкам такую функцию и не встретишь.

anonymous
()
Ответ на: комментарий от anonymous

Служит демагогическим «аргументом», очевидно же. Кроме как в посте анонимного эксперта по скобкам такую функцию и не встретишь.

Зато встретишь кучу альтернативного хаскиарта.

anonymous
()
Ответ на: комментарий от anonymous

И это, обычно, хорошо читаемый код. То, что для вас, не знающего хаскеля, нет разницы - это нормально. Кому надо (программисту на хаскеле, в данном случае) - тот легко разберется. Это, разумеется, и наоборот работает. Мне, как человеку к схеме непривычному, очень сложно прочесть "(define (ff y z) (f x y z)) (define (gg y) (g x y)) (lambda (x y) (ff (gg x) y))", а «f x . g x y» - легче легкого.

anonymous
()
Ответ на: комментарий от anonymous

И это, обычно, хорошо читаемый код.

Чушь собачья.

Кому надо (программисту на хаскеле, в данном случае) - тот легко разберется.

если в коде надо разбираться - значит он написан как говно, да.

а «f x . g x y» - легче легкого

Сколько аргументов у ф-и f?

anonymous
()
Ответ на: комментарий от anonymous

Чушь собачья.

Чего вы так разволновались-то вдруг?

если в коде надо разбираться - значит он написан как говно, да.

Забавно. По вашему, код не «написан как говно» только в том случае, если он не написан?

Сколько аргументов у ф-и f

Вы с таким упорством этот вопрос раз за разом задаете - будто бы надеетесь другой ответ получить. Аргумент у функции f один, как впрочем и у любой другой функции.

anonymous
()
Ответ на: комментарий от anonymous

Чего вы так разволновались-то вдруг?

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

Забавно. По вашему, код не «написан как говно» только в том случае, если он не написан?

Нет, в том случае, если в нем не надо «разбираться».

Вы с таким упорством этот вопрос раз за разом задаете - будто бы надеетесь другой ответ получить. Аргумент у функции f один, как впрочем и у любой другой функции.

Это с формальной точки зрения, что никого на практике не интересует. Речь идет о том, какое у нее количество аргументов согласно семантике. Например, у функции + - два аргумента, map - тоже два, printStrLn - один, fold - три, ну и так далее.

anonymous
()
Ответ на: комментарий от anonymous

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

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

Нет, в том случае, если в нем не надо «разбираться».

Ну так это бывает в одном тривиальном случае, когда код такой: "". Ну или вы под словом «разбираться» что-то сверхъестественное подразумеваете. Я считаю, что это означает «прочесть и понять смысл».

Это с формальной точки зрения, что никого на практике не интересует.

Нет, в хаскеле, представьте себе, это на практике так и есть.

Речь идет о том, какое у нее количество аргументов согласно семантике.

Пардон?

Например, у функции + - два аргумента, map - тоже два, printStrLn - один, fold - три, ну и так далее.

Как интересно. Вы только забыли уточнить о каком именно языке сейчас говорите.

anonymous
()
Ответ на: комментарий от anonymous

А вот эмоциональное высказывание несогласия с фактом, как в вашем случае, всегда таким признаком являлось.

Там не было моционаьного несогласия - там как раз и была констатация.

Я считаю, что это означает «прочесть и понять смысл».

Вопрос в том, сколько усилий на это уходит. С поинтфри - больше, чем без него.

Нет, в хаскеле, представьте себе, это на практике так и есть.

Так есть, но никому этот факт не важен, вот о чем я. На практике нам важна семантика ф-и, а не то, как она представлена на низком уровне. То есть на практике нет разницы между ф-ей f x y z = ... которая каррирована, или ф-и которая принимает кортеж из трех элементов, или ф-и, которая принимает список из трех элементов и т.д..

Пардон?

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

Как интересно. Вы только забыли уточнить о каком именно языке сейчас говорите.

О Haskell.

anonymous
()
Ответ на: комментарий от anonymous

map show [1, «2»]

В GHC 7.4.1 можно добиться такого:

> map show ([he|1, "2"|] :: [Any Show])
["1","\"2\""]
> map id ([he|1, "2"|] :: [Any Show])
[1,"2"]
> map (+ 1) ([he|1, 2|] :: [Any ShowNum])
[2,3]
> map show ([he|1, 2|] :: [Any ShowNum])
["1","2"]
> map id ([he|1, 2|] :: [Any ShowNum])
[1,2]

инстансы для Show/Num/ShowNum писать придётся - Any это полноценный тип, чужой интерфейс для него волшебным образом не заработает. Возмоность не определять ShowNum будет только, когда в GHC появится большая лямбда над классами.

quasimoto ★★★★
() автор топика
Ответ на: комментарий от anonymous

Так уже неоднократно делали.

Нет. Во-всяком случае, ничего юзабельного я не встречал, хотя пытался отыскать.

Ты прекрасно понял, что я имел ввиду.

Нет. Иначе не спрашивал бы.

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

Нам во втором случае достоверно известно, что у parseData второго аргумента нет.

То есть, что значение parseData x - не функция? С чего ты решил? Где это в коде?

Хватит под дурачка косить.

Согласен. Прекращай.

Miguel ★★★★★
()
Ответ на: комментарий от x4DA

То есть, scheme - что-то вроде ассемблера.

Где тут мемориз?

Miguel ★★★★★
()
Ответ на: комментарий от anonymous

Речь идет о том, какое у нее количество аргументов согласно семантике.

Согласно семантике у неё ОДИН аргумент. Как и у всякой другой.

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

Miguel ★★★★★
()
Ответ на: комментарий от anonymous

Там не было моционаьного несогласия

Ну, теперь отрицание, да еще и по буквам попадать перестали. Дышите глубже, вы взволнованы!

Вопрос в том, сколько усилий на это уходит. С поинтфри - больше, чем без него.

Вопрос верный. Ответ - нет. С пойнтфри усилий уходит, обычно, меньше. Поэтому такой подход и используется.

Так есть, но никому этот факт не важен, вот о чем я.

Дело в том, что вы ошибаетесь. Этот факт очень важен именно на практике и облегчает функциональное программирование - т.е. комбинирование одних функций с помощью других и получение в результате третьих. Попробуйте написать что-нибудь на ФЯ и поймете, о чем я сейчас говорю.

Ну например семантика ф-и map - она принимает функцию f, список lst и возвращает новый список, который состоит из элементов, полученных последовательным применением f к элементам lst.

Вы ошибаетесь. Ф-я map «поднимает» функцию в контейнер. Делает из функции, преобразующей один элемент списка в другой, функцию, преобразующую один список в другой. А уже эта функция может принять список, а может быть преобразована какой-нибудь другой функцией или скомбинирована с третьей и так далее. А что там будет происходить на низком уровне мы тут, по вашим же словам, не обсуждаем.

О Haskell.

В таком случае ваши рассуждения опять содержат фактические ошибки.

anonymous
()
Ответ на: комментарий от anonymous

Дело в том, что вы ошибаетесь. Этот факт очень важен именно на практике

Подозреваю, что от применения функции foldl к четырём и более аргументам он вообще в обморок упадёт.

Miguel ★★★★★
()
Ответ на: комментарий от anonymous

это чтобы код понять надо схему рисовать?

Это коммутативная диаграмма с экспоненциалами :) В лямбда исчислении терм ведь можно представить стрелкой. По крайней мере, f x . g x y это кусок диаграммы, которую можно как угодно достраивать влево вверх, так что спрашивать сколько у f аргументов (точнее, сколько нужно провести аппликаций, чтобы получить терминальное значение, точнее, стрелку в терминальный объект) это всё равно что спрашивать как можно достраивать эту диаграмму слева сверху - как угодно, количество аппликаций будет зависеть от того частью какой диаграммы (вплоть до стрелок в терминальные объекты) этот кусок является. В общем случае количество аргументов (необходимых аппликаций для f) >= 2.

Ну и не существует хардкода с нечастичными применениями и некаррированными лямбдами который позволили бы написать аналог такого выражения (можно написать только для конкретного колличества аргументов у некаррированной f).

quasimoto ★★★★
() автор топика
Ответ на: комментарий от quasimoto

влево вверх

Вниз:

          y ::
          *
    x ::  |
    *---->|
          v
          * g x y ::
    x ::  |
    *---->|
          v
          * f x . g x y ::
          |
          |
          v
      *------>*
      z ::    (f x . g x y) z ::
> ((\x y -> x + y) 1 . id) 1
2
> ((\x y z -> x + y * z) 1 . id) 1 2
3
quasimoto ★★★★
() автор топика
Ответ на: комментарий от quasimoto
\f -> f undefined . id :: (a -> b -> c) -> b -> c
-- f :: a -> b -> c ~ a -> b -> d -> e ~ a -> b -> d -> e -> f ~ ...
-- т.к. a -> b -> c == a -> (b -> c), с - любое.
quasimoto ★★★★
() автор топика
Ответ на: комментарий от quasimoto

так что спрашивать сколько у f аргументов (точнее, сколько нужно провести аппликаций, чтобы получить терминальное значение, точнее, стрелку в терминальный объект) это всё равно что спрашивать как можно достраивать эту диаграмму слева сверху - как угодно

О чем и речь. То есть информация о количестве аргументов пропала.

anonymous
()
Ответ на: комментарий от anonymous

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

Даже у top-level функции:

foo :: a -> b

количество аргументов (необходимых для полного применения) не факт что равно одному, т.к. forall b, в том числе b может быть (c -> d), (с -> d -> e) и т.д. количество аргументов у f >= 1.

quasimoto ★★★★
() автор топика
Ответ на: комментарий от quasimoto

Ну вот, и для id понятие «количество аргументов» в смысле «количество аппликаций до полного применения» не имеет смысла - оно различно в разных выражениях. Аргумент всегда один - домен.

id id id 3 == ((id id) id) 3 = ((id id') id'') 3

                      Int       Int
                      *--id''-->*
    (Int -> Int) *         |
                 |         |
             id' |---id--->|
                 v         |
    (Int -> Int) *         |
                           v
               3 :: Int *----->* 3 :: Int

// после стирания полиморфного типа.
quasimoto ★★★★
() автор топика
Ответ на: комментарий от anonymous

Если реализовано в GHC - значит есть в языке, который реализует GHC - т.е. в хаскеле.

Еще раз прочитай, в этом случае сравнение со схемой точно не корректно.

И пожалуйте прув цитатой, где речь идет о реализациях, пока в 3.7 вижу только контекст «языка» (то есть скорее стандарт, основанный на haskell-98).

Или по-вашему любая реализация с нестандартными фичами - определяет свой над-язык? Если и так, то это явно не общепринятое понятие «языка». И в этом случае корректно уточнять, что речь идет о «Glasgow Haskell».

anonymous
()
Ответ на: комментарий от Miguel

Из-за каррирования и поинт-фри записи теряется куча информации, которая при нормальной записи всегда присутствует в коде

Слушай, ты юниксовую командную строку, вообще, в глаза видел?

Вы таки полагаете, что пайпы - верх понятности, а не «быстро-и-грязно»?

anonymous
()
Ответ на: комментарий от anonymous

Я таки полагаю, что пайпы — это очень понятно. Я не понимаю, что значит «быстро и грязно» — им есть альтернатива?

Miguel ★★★★★
()
Ответ на: комментарий от quasimoto

Ну вот, и для id понятие «количество аргументов» в смысле «количество аппликаций до полного применения» не имеет смысла - оно различно в разных выражениях.

потому что нам не важно «количество аппликаций до полного применения». Нам важна семантика ф-и, согласно которой у id один аргумент.

anonymous
()
Ответ на: комментарий от anonymous

Нам важна семантика ф-и, согласно которой у id один аргумент.

Как и у всех остальных функций.

Miguel ★★★★★
()
Ответ на: комментарий от anonymous

Нам важна семантика ф-и, согласно которой у id один аргумент.

А у const сколько?

> :t const
const :: a -> b -> a -- два?
> :t const :: a -> (b -> a) -- один?
const :: a -> (b -> a) :: a -> b -> a
> :t const (+)
const (+) :: Num a => b -> a -> a -> a -- три?
> const 1 2 -- == (const 1) 2
1
> const (+) id 1 2 -- == (const (+) id) 1 2 == (((const (+)) id) 1) 2
3
quasimoto ★★★★
() автор топика
Ответ на: комментарий от anonymous

const принимает один аргумент и возвращает функцию, которая всегда возвращает то, что передали сonst.

anonymous
()
Ответ на: комментарий от anonymous

Он принимает _два_ аргумента и возвращает первый

Нужно дать определения понятиям «функция» и «количество аргументов». Например так - функция это бинарное отношение между множествами, количество аргументов (arity) функции это размерность домена этого отношения (размерность декартового произведения). Так, у функции uncurry const размерность домена = 2, у функции const = 1.

quasimoto ★★★★
() автор топика
Ответ на: комментарий от quasimoto

Нужно дать определения понятиям «функция» и «количество аргументов». Например так - функция это бинарное отношение между множествами, количество аргументов (arity) функции это размерность домена этого отношения (размерность декартового произведения). Так, у функции uncurry const размерность домена = 2, у функции const = 1.

Нет, не надо ничего давать. Мы говорим об интерпретации (наивной, ну как «стандартная модель арифметики», которая строгим понятием не является, но все интуитивно понимают, что она значит), а не о теории.

Вот берем, например, функцию map - как тут уже сказали, можно на нее смотреть как на комбинатор, который поднимает произвольную ф-ю в монаду List, а можно - как на ф-ю, которая применяет первый аргумент к элементам второго.

Эти два взгляда порождают две совершенно разные ф-и (ф-я, которая принимает двуместный кортеж - это уже третья _другая_ функция). В известной смысле все эти ф-и эквивлаентны - но они разные. Каррированный ЯП позволяет реализовать map в первом смысле, но не позволяет реализовать map во втором (он недостаточно выразителен). То есть при определении ф-и мы упускаем целый пласт информации о ее сущности. Да, для того же map это и не так страшно - потому что, как ясно из вышескзанного, оба взгляда вполне осмысленны. А вот с каким-нибудь (parseData data method) все уже не так безоблачно. Короче, количество аргументов в данном случае - это просто часть спецификации.

anonymous
()
Ответ на: комментарий от anonymous

Эти два взгляда порождают две совершенно разные ф-и

С одинаковым кодом.

Отсыпь травы.

А вот с каким-нибудь (parseData data method) все уже не так безоблачно.

В чём проблема-то?

Короче, количество аргументов в данном случае - это просто часть спецификации.

Угу. Либо 0, либо 1.

Miguel ★★★★★
()
Ответ на: комментарий от anonymous

Каррированный ЯП позволяет реализовать map в первом смысле, но не позволяет реализовать map во втором

Во втором это вот так:

map' :: (a -> b, [a]) -> [b]
map' = uncurry map

?

он недостаточно выразителен

Наоборот - можно писать функции из кортежей в значения и этим покрыть все возможности (с точки зрения функциональных абстракций и аппликаций) статически-типизированного ЯП без каррированных функций (в динамическом могут быть ещё функции многих аргументов и apply, впрочем, в хаскеле функции многих аргументов тоже пишутся и в apply при этом нет потребности), а можно начать использовать карринг. В ЯП без каррированных функций, например в Scheme, так ведь не получится. Можно только писать обычные функции (из кортежей в значения). Разве что с достраиванием самого синтаксиса - вводить новую лямбду определяющую каррированную функцию и новую аппликацию частично её применяющую, и будет язык с двумя вариантами лямбд и аппликаций (как @ из того макроса), вместо одного (как в хаскеле), плюс, эти каррированные функции будут тормозить, т.к. компилятор не будет проводить для них спецификацию.

quasimoto ★★★★
() автор топика
Ответ на: комментарий от quasimoto

будет язык с двумя вариантами лямбд и аппликаций

Вместо того, чтобы (f a b c) было применением каррированных функций, т.е. (((f a) b) c), а f (a, b, c) - применением функции к кортежу. Случай (f a b c) полностью, и по смыслу и по виду, соответствует тому как оно в лямбда исчислении (где функции каррированные), а случай f(a, b, c) - как в языках Pascal, C и т.д. (где функции некаррированные).

quasimoto ★★★★
() автор топика
Ответ на: комментарий от quasimoto

Во втором это вот так:

Нет, во втором это так: (define (map f lst) ...)

В каррированном ЯП невозможно напистаь аналог такой ф-и.

Наоборот - можно писать функции из кортежей в значения и этим покрыть все возможности (с точки зрения функциональных абстракций и аппликаций) статически-типизированного ЯП без каррированных функций

Нет, нельзя. В каррированном ЯП можно написать только ф-ю от одного аргумента, которая либо возвращает другую ф-и, либо принимает кортеж. Но написать ф-ю от многих аргументов нельзя.

впрочем, в хаскеле функции многих аргументов тоже пишутся

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

В ЯП без каррированных функций, например в Scheme, так ведь не получится. Можно только писать обычные функции (из кортежей в значения).

В ЯП типа схемы можно писать ф-и всех трех видов - обыкновенные, каррированные, ф-и от кортежей (бтв, зачем выделять ф-и от кортежей в отдельный класс? ведь любая ф-я от кортежа суть ф-я одного аргумента, которым является этт кортеж). В ЯП типа хаскеля - только последние два.

Разве что с достраиванием самого синтаксиса - вводить новую лямбду определяющую каррированную функцию и новую аппликацию частично её применяющую, и будет язык с двумя вариантами лямбд и аппликаций

Зачем? (lambda (x) (lambda (y) (+ x y))) - вот тебе каррированный плюс.

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

Можно и без тормозов (по крайней мере без тормозов в 99% юзкейсов, а если типизация статическая, то можно покрыть и последний 1%):

#lang racket

(require (for-syntax syntax/parse
                     srfi/1)) 

(define-syntax ($case-lambda stx)
  (syntax-parse stx
    [($case-lambda (name arg1 arg ... arg2) (args ...) body ...)
     (with-syntax ([((left ...) ...) (map reverse (pair-fold cons '() (reverse (syntax->list #'(arg1 arg ...)))))]
                   [((right ...) ...) (reverse (pair-fold cons '() (syntax->list #'(arg ... arg2))))])
       #'(case-lambda 
           [(left ...) ($case-lambda (name right ...) (args ...) (name args ...))] ...
           [(arg1 arg ... arg2) body ...]))]
    [($case-lambda (name arg1) (args ...) body ...) 
     #'(lambda (arg1) (name args ...))]))

(define-syntax ($define stx)
  (syntax-parse stx
    [(_ (name:id arg1:id arg:id ... arg2:id) body:expr ...+) 
     #'(define name ($case-lambda (name arg1 arg ... arg2) (arg1 arg ... arg2) body ...))]
    [(_ (name:id arg:id ...) body:expr ...+) 
     #'(define (name arg ...) body ...)]))



Собственно, если заменить теперь стандартную case-lambda на $case-lambda, а lambda выразить через нее (как case-lambda с одним кейзом) и потом бутстрапнуть - то будет каррированный диалект. Ну еще примитивные ф-и придется переписать, конечно, но это в любом случае.
anonymous
()
Ответ на: комментарий от quasimoto

а случай f(a, b, c) - как в языках Pascal, C и т.д. (где функции некаррированные).

Нет, f (a, b, c) хаскеля соответствует f(x), где х - какой-то record/структура. То, что в паскале/С записывается f(a,b,c) в хаскеле невыразимо по той причине, что там (f a b c) = f a $ b $ c (вообще в лямбда-исчислении не бывает (f a b c), это сахар для (((f a) b) c)). В некаррированных ЯП эти записи различны.

anonymous
()
Ответ на: комментарий от quasimoto

map' = uncurry map

К слову, ф-и uncurry не существует. В том смысле, что есть функция curry, которая преобразует некаррированную ф-ю в каррированную, но ей обратной (curry^-1) - нет.

anonymous
()
Ответ на: комментарий от anonymous

Функция многих аргументов - это функция, принимающая кортеж. Если считать, что функция, которая принимает кортеж - это функция одного аргумента, то тогда бывают функции только одного аргумента.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.