LINUX.ORG.RU

Пытаюсь разобраться в Haskell

 


1

2

Привет, ЛОР!
Возможно, мой вопрос довольно глупый, но очень хотелось бы всё-таки разобраться.
По какой причине в Haskell недопустимы конструкции типа Integer + Double? Если я правильно понял, дело в том, что (+) имеет тип Num a => a -> a -> a, т.е. оба аргумента должны быть одного типа. Но по какой причине так сделано, ведь они и так оба являются инстансами Num?

Deleted

потому, что Integer и Double это разные типы, если хочешь сложить приведи к тому, к которому нужно. напр.: a + round b или fromIntegral a + b.

qnikst ★★★★★
()
Ответ на: комментарий от Apple-ch

Здесь компилятор назначит 1 тип Double. А если указать Инт явно, то будет фейл:

Prelude> (1::Int) + 2.5

<interactive>:2:12:
    No instance for (Fractional Int) arising from the literal `2.5'
    Possible fix: add an instance declaration for (Fractional Int)
    In the second argument of `(+)', namely `2.5'
    In the expression: (1 :: Int) + 2.5
    In an equation for `it': it = (1 :: Int) + 2.5
provaton ★★★★★
()
Ответ на: комментарий от Apple-ch
λ> :t 1 + 2.5
1 + 2.5 :: Fractional a => a
λ> let k = 1 + 2.5
λ> :t k
k :: Double

просто в этом случае был выбран общий тип Fractional a, а потом приведен к типу по умолчанию Double.

qnikst ★★★★★
()

Вот что бывает, когда у человека в качестве первого ЯП выступает скриптовый недоязычок.

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

очень частный случай проявляется в 1+2.5

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

Окей.
Правильно ли я понимаю, что будь (+) определён как (Num a, Num b) => a -> b -> b или вообще (Num a, Num b, Num c) => a -> b -> c, то один тип не пришлось бы приводить к другому?

Deleted
()

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

KblCb ★★★★★
()

Потому что Haskell - язык со строгой типизацией.

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

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

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

fromIntegral перед Int/Integer? Да, я понимаю, что можно придумать свой инстанс Num и для него уже не будет никакого fromIntegral, но нельзя ли компилятору попытаться угадать ту функцию, которая привела бы оба типа к одному? Разумеется, с предупреждением о возможном повреждении данных.

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

С питоном у меня не сильно дела лучше, если что.

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

Ну вот, например, в порядке бреда:

instance Num Bool where
    x + _ = x
    x * _ = x
    abs x = x
    signum x = x
    fromInteger x | x == 0 = False
                  | otherwise = True

Это объявление абсолютно валидное, теперь операция True + False будет иметь для компилятора смысл. Но если я напишу True + 2.5, то что должен делать компилятор?

provaton ★★★★★
()
Последнее исправление: provaton (всего исправлений: 1)
Ответ на: комментарий от provaton

С одной стороны понимаю, почему так, с другой, не могу понять, почему тогда не реализован какой-нибудь абстрактный тип чисел вроде R или даже C из математики. Хотя ведь и это не предел абстракции…

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

А чёрт его знает. Написать, что не знает, как складывать Bool и Num. Но ведь как складывать числа из Int/Integer/Float/Double и так понятно, разве нет? Не менять тип (+), но реализовать сложение между этими типами хотя бы.

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

Но ведь как складывать числа из Int/Integer/Float/Double и так понятно, разве нет?

Нет. А именно что ты ожидаешь в результате - какой тип?

1 + 2.5 = 3.5 или 3?

Ты вообще просишь то против чего собственно вводилась статическая типизация - находить ошибки. Если у тебя большая сложная формула с умножениями делениями и прочими корнями - то в случае коэршена кудапопало как в прочих сях - ты получишь «гдэ-то 7 - гдэ-то 8 что-то около». Такого рода решения противоречат идеологии языков которые заточены на очень строгие правила типизации чтобы находить все эти ошибки.

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

Но ведь (если не вдаваться в конкретные реализации) 1 — это есть 1.0? Тогда 1 + 2.5 должно выдавать 3.5. Или это только мне логичным кажется?..

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

Тогда 1 + 2.5 должно выдавать 3.5. Или это только мне логичным кажется?..

Только тебе. Есть еще всякие сдвиги. Что будет если ты результат это сдвинешь влево? Еще учти что точность представления чисел с плавающей точкой - не бесконечная. В С проблемы такого рода решают в виде «херня какая-то считается - давай искать где...ага! вот сдесь сделаем кастинг к лонгу!». Хаскаел by design заточен на то чтобы такого рода проблем - не было. То есть ожидается что ты однозначно решишь какого рода операции это будут - целочисленные или нет - иначе не скомпилится. Чтобы не получалось 3.5 `shiftL` 8.

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

Предположим, когда нужны побитовые операции, приводить числа к конкретным значениям. При математических же операциях работать с абстрактными числами, когда 1 + 2.5 без сомнений выдаст 3.5.
Нет, причину поведения я понял, вопрос закрыт.

Deleted
()

В общем попробую ответить подробно:

1). Num это class, т.е. если проводить аналогии из «привычных» языков - это интерфейс. Он говорит о том, что данный тип является числом и поддерживает операции для чисел. Классы не являются инъкективными, т.е. если мы знаем, что тип является экзампляром класса, то мы ничего не можем сказать о типе, кроме того, что над ним можно выполнять ряд операций.

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

3). у нас могут быть интересные случаи, если числовые типы друг к другу не сводятся:

newtype Кошка = Кошка Int
newtype Литр  = Литр Int
> (3::Кошка) + (5::Литр) = WTF????

представим, что у нас есть ещё тип:

newtype Спирт = Спирт Int

к которому мы можем свести как кошку, так и литр, мы думали, что WTF должно обработаться как:

вСпирт (3::Кошка) + вСпирт (5::Литр) = 19::Спирт

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

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

class MNum a b where
  type MNumT :: c
  fromA :: a -> c
  fromB :: b -> c
  (+) :: (Num c) => a -> b -> c
  a + b = fromA a + fromB c

дальше для всех пар a b определяешь MNumT, будет очень много и просто не нужно. В сях компилятор решает простейшие случаи.

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

Огромное спасибо за разъяснение, так вопросов больше не имею :)

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

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

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

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

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

легко, правда писать полноценный Num2 класс мне было лень и я написал только плюс, но остальное делается аналогично:

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-}
import Data.Function
newtype Баран = Баран {неБаран :: Double} deriving Show
newtype Стул  = Стул {неСтул :: Int} deriving Show

instance Num Баран where
    (+) = (Баран .). ((+) `on` неБаран)
    (*) = (Баран .). (*) `on` неБаран
    (-) = (Баран .). (-) `on` неБаран
    abs = Баран . abs . неБаран
    signum = Баран . signum . неБаран
    fromInteger = Баран . fromInteger

instance Num Стул where
    (+) = (Стул .). ((+) `on` неСтул)
    (*) = (Стул .). (*) `on` неСтул
    (-) = (Стул .). (-) `on` неСтул
    abs = Стул . abs . неСтул
    signum = Стул . signum . неСтул
    fromInteger = Стул . fromInteger

class Coerce a b where
   coerce :: a -> b

instance Coerce Баран Стул where
    coerce (Баран x) = 
      let r = round $ x/8
          f = floor $ x/8
      in Стул (-r-(if r == f then 1 else 0))-- баран ест 1/8 стула

instance Coerce Стул Баран where
    coerce (Стул x) = Баран ((exp 1)*fromIntegral x) -- стул призывает e баранов


instance Coerce a a where
    coerce = id
λ> (Баран 1) +%+ (Стул 2) :: Баран
Баран {неБаран = 6.43656365691809}
λ> (Баран 1) +%+ (Стул 2) :: Стул
Стул {неСтул = 1}
qnikst ★★★★★
()
Ответ на: комментарий от qnikst

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

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

Num обозначает, то что данный тип ведёт себя как число, т.е. для него имеют смысл операции:

class Num a where
  (+) :: a -> a -> a
  (*) :: a -> a -> a
  (-) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a

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

newtype MadInt = MadInt Int
instance NumMad Int where
  (MadInt a) + (MadInt b) =
      case a + b `mod` 3 of 
         0 -> MadInt $ a+b+1
         1 -> MadInt $ a+b
         2 -> MadInt $ a-b
  (MadInt 0) * (MadInt 0) = 42
  a          * (MadInt 0) = 0
  (MadInt 0) * b          = 0
  (MadInt a) * b          = b + (MadInt (a-1))*b
  ...                          
qnikst ★★★★★
()
Ответ на: комментарий от qnikst

Ага, т.е. в математике есть примеры нестандартного поведения. Тогда всё понял, ещё раз спасибо :)

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

я не математик и такого подобного поведения не видел, просто это хороший пример того, что мозг пытается упростить ситуацию, до наглядного примера, и достаточно широкая и сложная область не покрывается. Т.е. тут мозг предполагает существование отношения на типах A<B, и операций перевода между типами, таким образом, что мы можем просто выбрать «больший» тип и поднять меньший в него неявно, или если указать типы, то опустить больший тип в указанный. В целом явное приведение типов гораздо безопаснее неявного, т.к. мы обходим, как проблемы представления (double<<1), так и логические проблемы, правда цена этого в том, что приходится писать больше кода.

Вообще может в этот тред заглянет quasimoto и распишет всё с правильной терминологией и с интересными примерами, у него образование по данному направлению сильно лучше, чем у меня :)

qnikst ★★★★★
()
Последнее исправление: qnikst (всего исправлений: 1)
Ответ на: комментарий от qnikst

может в этот тред заглянет quasimoto и распишет всё с правильной терминологией

Распишет так, что никто ничего не поймет :)

alienclaster ★★★
()

По какой причине в Haskell недопустимы конструкции типа Integer + Double?

Потому что не существует такой подстановки которая превратила бы

forall a. Num a => a -> a -> a

в

Integer -> Double -> ? 
fmap
()
Ответ на: комментарий от r

Такого рода решения противоречат идеологии языков которые заточены на очень строгие правила типизации чтобы находить все эти ошибки

Такого рода решения противоречат идеологии языков которые заточены на очень строгие правила типизации чтобы находить все эти ошибки

Такого рода решения противоречат идеологии языков которые заточены на очень строгие правила типизации чтобы находить все эти ошибки

Такого рода решения противоречат идеологии языков которые заточены на очень строгие правила типизации чтобы находить все эти ошибки

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