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).

★★★★

почитал тред и появилась уверенность что хаскель не для меня. Ни в этой жизни, ни в следующей. Брейнфак какой-то))

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

Функция многих аргументов - это функция, принимающая кортеж.

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

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

Поправка - тогда _в хаскеле_ бывают только функции одного аргумента (формально).

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

Поправка должна быть не тут. В хаскеле есть функции в математическом смысле. В сишке их нет, но есть процедуры.

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

В хаскеле есть функции в математическом смысле. В сишке их нет, но есть процедуры.

В сишке функции есть в математическом смысле на столько же, на сколько они есть в хаскеле. Надо только world неявно передать.

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

Неявно передовать world не достаточно.

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

Ты внимательно читай. Речь шла о том, что не существует uncarry = carry^-1, то есть такой f, что uncarry . carry = id. Не существует ее по одной простой причине - carry неинъективна (например, если применить ее к (a -> b) и к ((a) -> b) получим один и тот же результат - (a -> b)). А для неинъективных ф-й обратных не существует.

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

если применить ее к (a -> b) и к ((a) -> b)

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

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

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

Не шути так, это лисп. Аналог я привёл.

В каррированном ЯП можно написать ... ф-ю ... которая ... принимает кортеж.

написать ф-ю от многих аргументов

Это одно и то же.

Нет, не пишутся.

Text.Printf.printf - что тогда это такое?

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

Опять шутки про препроморфизмы вне контекста?

В ЯП типа схемы можно писать ф-и всех трех видов - обыкновенные

Обыкновенные? Функция это бинарное отношение, т.н. обыкновенные функции в том же лиспе это просто сахар для вызова callable объекта на _одном_ объекте в который упакованы нужные аргументы (не так просто упакованы как в кортеж, но аналогично - этот объект суть cons-ы):

(f a b c) = (apply 'f '(a b c)).

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

Во-первых, вербозно:

(+)         (lambda (x) (lambda (y) (+ x y)))
(a +)       ((lambda (x) (lambda (y) (+ x y))) a)
            (lambda (y) (+ a y))
(+ b)       ; ?
            (lambda (x) (+ x b))

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

Можно и без тормозов

На макросах - можно, будет лисп с двумя лямбдами и аппликациями, или диалект в котором сделано _правильно_ :)

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

f (a, b, c) хаскеля соответствует f(x), где х - какой-то record/структура.

Ну да, и аргументы на стек оно ещё не кладёт по очереди. А вот в использовании - нет разницы.

К слову, ф-и uncurry не существует.

Математически (а то в хаскеле есть Prelude.uncurry)? Тогда пруф?

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

Например для id

(curry . uncurry) id == id
(uncurry . curry) id == id
quasimoto ★★★★
() автор топика

Кстати, а кто-нибудь из присутствующих использовал GObject из-под хаскеля? Или использовал связку хаскель/Vala?

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

Не шути так, это лисп. Аналог я привёл.

Нет, не привел.

Это одно и то же.

Нет, это две совершенно разные функции.

Text.Printf.printf

Функция от одного аргумента. Других в хаскеле нет.

Опять шутки про препроморфизмы вне контекста?

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

Обыкновенные?

Да, обыкновенные.

Функция это бинарное отношение

Это в математике. Но при чем тут математика, если мы говорим о ЯП? Собственно, в каждом ЯП есть свое собственное определение ф-й (причем оно _никогда_ не совпадает с математическим). Если примерно и в общем, то ф-я в ЯП - это определенный терм.

т.н. обыкновенные функции в том же лиспе это просто сахар для вызова callable объекта на _одном_ объекте в который упакованы нужные аргументы (не так просто упакованы как в кортеж, но аналогично - этот объект суть cons-ы):

Нет, не правда. Ф-я от консов и ф-я от многих аргументов - разные вещи.

(f a b c) = (apply 'f '(a b c)).

Ну да, из ф-и f, которая принимает три аргумента, можно сделать некую другую функцию, которая будет принимать один аргумент-список длины 3. Что дальше? Это ведь все равно другая функция.

Во-первых, вербозно:

Зато инофрмативно. Хороший стиль программирования - не экономить на документации. Оптимально - писать код, который является документацией сам к себе.

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

Не понял, что? Любые лямбды применяются в рантайме.

На макросах - можно, будет лисп с двумя лямбдами и аппликациями

Да не будет. Если есть ф-и с многими аргументами, то каррированные ф-и выражаются через них, так что достаточно одной лямбды. Вот если у нас только каррированные ф-и есть - то ф-и многих аргументов уже не выразишь, да. Так что необходимости во второй лямбде нет.

ли диалект в котором сделано _правильно_ :)

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

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

А вот в использовании - нет разницы.

Конечно нет, ведь все эти три ф-и (обычная, каррированная, от структуры) эквивалентны. Но не совпадают :)

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

Математически (а то в хаскеле есть Prelude.uncurry)?

При чем тут математика? Мы о ЯП.

Тогда пруф?

Ну я уже объяснил в чем тут дело, carry неинъективна. Как к ней обратную строить?

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

uncarry f' = f" (x, y, z)

Это какой-то бред. Написано безграмотно, но даже если попытаться придать этому какой-то смысл — будет чушь.

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

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

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

Но при чем тут математика, если мы говорим о ЯП?

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

Ф-я от консов и ф-я от многих аргументов - разные вещи.

В чём, собственно, принципиальная разница? Одно вполне может служить моделью для другого, абстракция протекать не будет.

Зато инофрмативно. Хороший стиль программирования - не экономить на документации.

Когда я учился в школе, у нас был один парень, на информатике писавший программы типа

var kolichestvo_vagonov_stojaschih_na_zapasnyh_putjah : integer;
Я собирался написать более многострочный пример, но, извини, не могу. Пальцы не поворачиваются.

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

Вот если у нас только каррированные ф-и есть - то ф-и многих аргументов уже не выразишь, да.

Для них хорошей моделью будут функции на туплах. Даже выглядеть будут так же.

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

Ну я уже объяснил в чем тут дело, carry неинъективна. Как к ней обратную строить?

Во-первых, прекрати называть curry словом carry.

Во-вторых, ты не доказал, что она неинъективна.

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

Функция от одного аргумента. Других в хаскеле нет.

Вот и договорились.

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

Я о таком никогда не слышал. Произвольное количество аргументов делается ad-hoc с помощью тайпклассов:

class MyFun t where
  myFun :: t

instance (a ~ b) => MyFun (a -> b) where
  myFun x = x

instance (Show a, Show b) => MyFun (a -> b -> IO ()) where
  myFun x y = putStrLn $ show x ++ "@" ++ show y

instance MyFun ((a, b, c) -> c) where
  myFun (_, _, z) = z
> myFun "foo"
"foo"
> myFun 5 "5" :: IO ()
5@"5"
> myFun(1, '2', "3")
(1,'2',"3")

Но при чем тут математика

Вот в хаскеле достаточно математического понимания функций (равно как и типов данных).

то ф-я в ЯП - это определенный терм.

Это не просто терм, это замкнутый терм + набор аргументов + контекст (если это замыкание).

Ф-я от консов и ф-я от многих аргументов - разные вещи.

Ну да, из ф-и f, которая принимает три аргумента, можно сделать некую другую функцию, которая будет принимать один аргумент-список длины 3.

(defun foo (x y z)
  (format t "~s ~s ~s~%" x y z))

(foo 1 2 3)
;; 1 2 3
;; NIL

(funcall #'foo 1 2 3)
;; 1 2 3
;; NIL

(apply #'foo '(1 2 3))
;; 1 2 3
;; NIL

это не некая другая функция.

Не понял, что? Любые лямбды применяются в рантайме.

(- x y)
(funcall (funcall #'(lambda (x) (lambda (y) (- x y))) x) y)

Тут в первом случае должен быть просто вызов (в соответствии с нужным CC) функции #'-, или даже инструкция SUB, во втором случае могут быть применения лямбд в рантайме (у SBCL не будет - он такое оптимизирует, но не знаю про более запутанные случаи). Т.е. это будет заботой либо компилятора, либо макроса добавляющего эти лямбды и аппликации.

Нет, не привел.

Нет, это две совершенно разные функции.

Вот если у нас только каррированные ф-и есть - то ф-и многих аргументов уже не выразишь, да.

Just because? Вот тут был приведён пример f x . g x y - как написать аналог на ванильной схеме (без макросов)? Т.е. информация о «количестве аргументов» у f и g тут потерялась. Ну и обратно, я могу писать f x y z или f (x, y, z) в ванильном хаскеле, и пока не было приведено примера чего-то что в хаскеле написать нельзя (не демагогически «функцию многих аргументов и чтобы не от кортежа», а конкретно).

Собственно, правильно (если уж по-честному) это как раз вариант с двумя лямбдами.

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

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

carry неинъективна

curry : (a × b → c) → (a → (b → c)). Пусть x ~ (a × b → c) и y ~ (a → (b → c)), приведи пример таких p : x ≢ q : x, что curry p : y ≡ curry q : y (чтобы доказать неинъективность, тут ещё нужно условится о классе эквивалентности, чтобы рассуждать о term equality (α-эквивалентность, ещё что-то?)).

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

чтобы доказать неинъективность, тут ещё нужно условится о классе эквивалентности, чтобы рассуждать о term equality (α-эквивалентность, ещё что-то?)).

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

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

Observational equality, естественно.

Если так, то чисто интуитивно curry выглядит инъективной - было бы странно, если бы две функции не равные в смысле observational equality будучи каррированными стали бы равными в этом смысле.

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

чисто интуитивно curry выглядит инъективной

Дык, вроде, она такая и есть.

Вот uncurry, кстати, не совсем инъективна (да, я знаю, что это примерно как «немножко беременна»). Но я думаю, что Позор Анонимусов не сообразит, что она склеивает.

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

Во-вторых, ты не доказал, что она неинъективна.

Я привел пример в котором curry (настоящая curry, не та, которая в хаскеле) на разных аргументах дает одинаковый результат. Значит - неинъективна.

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

Значит - неинъективна.

А вот как ты относишься к утверждению о том, что для всякой категории допускающей экпоненцирование (e.g. Set, Hask) существет биекция:

Hom(c × b, a) ≅ Hom(c, b → a)

?

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

Вот и договорились.

С точки зрения ЯП. С точки зрения программиста, конечно же, есть.

Произвольное количество аргументов делается ad-hoc с помощью тайпклассов:

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

Вот в хаскеле достаточно математического понимания функций (равно как и типов данных).

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

Это не просто терм, это замкнутый терм + набор аргументов + контекст (если это замыкание).

Ну я и сказал - _определенный_ терм. Какой именно - это уже вопрос открытый и зависит от ЯП.

это не некая другая функция.

Как не другая?

* (defun foo (x y z) (format t "~s ~s ~s~%" x y z))

FOO
* (foo 1 2 3)
1 2 3
NIL
* (funcall #'foo 1 2 3)
1 2 3
NIL
* (funcall #'foo '(1 2 3))

debugger invoked on a SB-INT:SIMPLE-PROGRAM-ERROR:
  invalid number of arguments: 1

(FOO (1 2 3))[:EXTERNAL]
0]
Четко видно - вызвать функцию f с аргументом-списком нельзя. Это приводит к ошибке. Просто apply из переданной ф-и от многих аргументов делает ф-ю от списка (_другую_ функцию) и потом ее уже применяет.

Т.е. это будет заботой либо компилятора, либо макроса добавляющего эти лямбды и аппликации.

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

Just because? Вот тут был приведён пример f x . g x y - как написать аналог на ванильной схеме (без макросов)?

Ну например так: (define (g x y z) ((f x) ((g x y) z))

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

В этом и проблема.

Ну и обратно, я могу писать f x y z или f (x, y, z)

Неправильно! В хаскеле ты можешь писать (((f x) y) z) и f (x, y ,z), а вот (f x y z) - не можешь (оно рассахаривается до первого варианта).

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

Был и не единожды - любая ф-я от многих аргументов. Эмулировать ее - можно, написать - нельзя.

(не демагогически «функцию многих аргументов и чтобы не от кортежа», а конкретно).

Хорошо, конкретно - функция «+», которая принимает два аргумента и возвращает сумму. При попытке вызова с одним аргументов должна быть ошибка, естественно.

а в языке с синтаксисом можно просто взять каррированные функции и синтаксически удобные кортежи

Но ведь это неудобно и костыльно (вплоть до полной неюзабельности - никто не использует твои «синтаксически удобные кортежи»). Два варианта лямбд - куда удобнее, лучше, менее вербозно и т.п.. Проблема тут в том, что функции должны быть некаррриованными _по дефолту_, и каррированными только в исключительных случаях (по аналогии, кстати с ленивостью - функции должны быть строгие по дефолту и ленивые в исключительных случаях, по аналогии с чистотой - функции должны быть грязными по дефолту и чистыми в исключительных случаях и так далее. тут проявляется критический недостаток в идеологии хаскеля).

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

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

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

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

Дык, вроде, она такая и есть.

Я говорил о настоящей curry (которая берет любую функцию и возвращает ее каррированный вариант). Дальше все очевидно: curry . curry = curry (то есть если применить каррриование к уже каррированной ф-и, то ничего не изменится), если существует обратная, то: uncarry . curry . curry = uncarry . curry => curry = id, что неверно.

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

curry : (a × b → c) → (a → (b → c)).

Неправильно! curry : narg-function -> 1arg-function. Вот такой у нее тип. Функция, как мы уже условились, полноценная - то есть класс эквивалентности определенных термов, а не чушь всякая на счет бинарного отношения.

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

Но при чем тут curry?

Для всех объектов a, b, c и стрелок g : a × b → c существует объект (b → c) (у нас что Set, что Hask - CCC), стрелки eval : (b → c) × b → c и (единственная) λg : a → (b → c), такие что:

     (b → c) × b -- eval --> c
             ^
             |
             .
  λg × id(b) |
             .
             |
             .
           a × b -- g -----> c

При этом функция задающая биекцию Hom(a × b, c) ≅ Hom(a, b → c) это curry, она инъективна (λg = λf ⇒ eval ∘ λg × id(b) = eval ∘ λf × id(b) ⇒ g = f) и сюръективна (g = eval ∘ f × id(b) ⇒ f = λg). Переформулируя с языка функций на категорный curry/uncurry это сопряжённые функторы заданные на –×b и b→–.

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

(define (g x y z) ((f x) ((g x y) z))

Я хотел аналог

h f g x y = f x . g x y

Хорошо, конкретно - функция «+», которая принимает два аргумента и возвращает сумму. При попытке вызова с одним аргументов должна быть ошибка, естественно.

> let sum = uncurry (+)
> sum (1, 2)
3

При попытке вызова с одним, но не Num a => (a, a) будет ошибка.

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

Ничего не мешает определить нужные комбинаторы (что можно повсюду наблюдать) и не использовать лямбд вообще - только top-level и локальные, в let / where, функции.

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

Ничего не мешает

А вот связывание и замена переменных в комбинаторном языке - вопрос.

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

curry . curry = curry

Не равно. У них даже типы не матчатся (там где a × b и a × b × с). Рассуждение не строгое, как будто ты лиспо-логикой пользуешься ;)

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

Класс эквивалентности?

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

uncarry

Кстати, тоже через U пишется.

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

Я хотел аналог h f g x y = f x . g x y

(define ((h f g x y) z) ((f x) ((g x y) z))

let sum = uncurry (+)

проверяем:

Prelude> let sum = uncurry (+)
Prelude> let x = (1, 2)
Prelude> sum x
3
Prelude>
То есть это ф-я одного аргумента. Требовалось два.

Ничего не мешает определить нужные комбинаторы (что можно повсюду наблюдать) и не использовать лямбд вообще - только top-level и локальные, в let / where, функции.

конечно не мешает, но выглядит это как убогий костыль. Особенно когда начинатся композиции всякие и т.п.. Допустим в хаскеле надо f . g, f .: g и так далее, в конкантенативном ЯП этого ничего не надо. Просто пишем g f. и никаких комбинаторов определять тоже не надо. Тут суть простая - базовая операция аппликативных языков - это аппликация f x. А аппликация производится к аргументам. Базовая операция конкантенативных языков - это обратная композиция. И она записывается так же - f g. Поинт-фри в аппликативных ЯП изначально ущербно, потому что мы все равно никуда не можем уйти от аппликаций. Определение любой ф-и все ранво необходимо задавать через аппликацию, никаких других способов нет.

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

Не равно. У них даже типы не матчатся

Матчатся. curry : narg -> 1arg. 1arg < narg => curry . curry определена и curry . curry : narg -> 1arg. то есть тот же, что и у curry.

Класс эквивалентности?

У нас разные термы могут задавать одну и ту же функцию, так? Как минимум, не следует различать термы, у которых с точностью до альфа-конверсии совпадает нормальная форма.

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

Опа. А какая тогда?

В хаскеле огрызок, который curry : 2arg -> 1arg. В лиспе полноценная curry, например.

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

Я говорил о настоящей curry

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

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

В хаскеле огрызок, который curry : 2arg -> 1arg. В лиспе полноценная curry, например.

Выучи, наконец, что в хаскеле любая функция имеет один аргумент. Нет никаких 2arg. То, что ты пытаешься тут придумать, просто бессмысленно.

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

При чем тут хаскелья? Я говорю о людских ЯП и о функции curry, которая принимает ф-ю любого количества аргументов и возвращает ф-ю от одного аргумента. Что там происходит в вашем петушином хаскеле меня не ебет.

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

Строго: curry : narg -> arg1 & (f : narg => ( ... (((curry f) arg1) arg2) ... argn) = (f arg1 ... argn))

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

Я говорю о людских ЯП и о функции curry, которая принимает ф-ю любого количества аргументов и возвращает ф-ю от одного аргумента. Что там происходит в вашем петушином хаскеле меня не ебет.

Ну дык никого не ебёт, что в твоём петушином лиспе какая-то функция оказалась неинъективна. Быдлопроблемы.

Строго: curry : narg -> arg1 & (f : narg => ( ... (((curry f) arg1) arg2) ... argn) = (f arg1 ... argn))

Что означает этот набор значков?

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

Это все ясно, но curry тут при чем?

Там написано при чём. Если строго, то curry это часть биекции (инъективная и сюръективная) из стрелок Hom(a × b, c) в стрелки Hom(a, b → c). И соответствующий сопряжённый функтор. Любой другой curry это просто какая-то вольность речи, спорить до посинения о которой нет смысла.

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

Ну дык никого не ебёт, что в твоём петушином лиспе какая-то функция оказалась неинъективна. Быдлопроблемы.

Ну дык а в вашем хаскеле ее вообще нет.

Что означает этот набор значков?

Формально строгое определение функции curry.

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