LINUX.ORG.RU

История изменений

Исправление Miguel, (текущая версия) :

В своё время я об этом заводил баг — мне прямым текстом сказали, что мономорфизация в Rust by design и нефиг раскачивать лодку. Правда, это было несколько месяцев назад — но недавно мне в уши надули, что, якобы, появились настоящие дженерики, и я попробовал заново.

struct Nil;
struct Cons<T> {
  head : int,
  tail : T
}
trait Product {
  fn product(&self, other: Self) -> int;
}
impl Product for Nil {
  fn product(&self, _other: Nil) -> int {0}
}
impl<T : Product> Product for Cons<T> {
  fn product(&self, other: Cons<T>) -> int {
    self.head * other.head + self.tail.product(other.tail)
  }
}
fn genprod<T : Product>(x : T, y : T, n : int) -> int {
  match n {
    0 => x.product(y),
    _ => genprod(Cons {head : n+1, tail : x}, Cons {head : n*n+2, tail: y}, n-1)
  }
}
fn essence(n : int) -> int {genprod(Nil, Nil, n)}
fn main() {
  println!("{}", essence(5));
}
Ну и получил, соответственно,
test.rs:17:1: 22:2 error: reached the recursion limit during monomorphization
test.rs:17 fn genprod<T : Product>(x : T, y : T, n : int) -> int {
test.rs:18   match n {
test.rs:19     0 => x.product(y),
test.rs:20     _ => genprod(Cons {head : n+1, tail : x}, Cons {head : n*n+2, tail: y}, n-1)
test.rs:21   }
test.rs:22 }

невозможность писать «специализации» полиморфных функций.

Ну дык на том стоит ML — дженерик-функция пишется один раз, и работает со всеми возможными типами. И в Rust всё точно так же.

Другой вопрос, что:

1) Компилятор (не обязательно rust) в НЕКОТОРЫХ СЛУЧАЯХ использует специализированную функцию — скажем, GHC вообще этим знаменит, он буквально для каждой полиморфной функции старается нагенерить побольше специализаций. Но этот процесс никак не контролируется программистом и вообще снаружи не виден (кроме того факта, что некоторые функции работают чуть побыстрее).

2) Есть возможность (используя всякие фокусы типа overlapping instances) сделать иную реализацию тайпкласса (это уже чисто Haskell-specific, ни Rust, ни ML-языки тут ни при чём). Типа

data T a = ...
class C x where ...
instance C (T a) where ...
instance C (T Int) where ...
В этом случае, если функции из тайпкласса C используются для типа T Int — то будет использоваться вторая реализация.

Однако, тут есть ловушка, которую не все понимают (и регулярно попадаются). Выбор инстанса производится на стадии компиляции. То есть, если использовать функцию из класса C в ситуации, когда конкретный тип неизвестен (и не может быть определён исходя из типов аргументов), то вне зависимости от того, окажется там на самом деле Int или что-то другое — выбрана будет общая реализация.

То есть, вот такой код:

{-# LANGUAGE ExistentialQuantification, FlexibleInstances, OverlappingInstances #-}
module Main where
data T a = T a
class C x where c :: x -> Int
instance C (T a) where c _ = 0
instance C (T Int) where c (T n) = n
data E = forall a. E a
test :: E -> Int
test (E a) = c (T a)
main =
  do print $ c $ T (4 :: Int)
     print $ test $ E (4 :: Int)
выведет
4
0

То есть, мы не можем утверждать, что для типа Int всегда работает специализированный вариант. Работает, но только в простых случаях.

Исправление Miguel, :

В своё время я об этом заводил баг — мне прямым текстом сказали, что мономорфизация в Rust by design и нефиг раскачивать лодку. Правда, это было несколько месяцев назад — но недавно мне в уши надули, что, якобы, появились настоящие дженерики, и я попробовал заново.

struct Nil;
struct Cons<T> {
  head : int,
  tail : T
}
trait Product {
  fn product(&self, other: Self) -> int;
}
impl Product for Nil {
  fn product(&self, _other: Nil) -> int {0}
}
impl<T : Product> Product for Cons<T> {
  fn product(&self, other: Cons<T>) -> int {
    self.head * other.head + self.tail.product(other.tail)
  }
}
fn genprod<T : Product>(x : T, y : T, n : int) -> int {
  match n {
    0 => x.product(y),
    _ => genprod(Cons {head : n+1, tail : x}, Cons {head : n*n+2, tail: y}, n-1)
  }
}
fn essence(n : int) -> int {genprod(Nil, Nil, n)}
fn main() {
  println!("{}", essence(5));
}
Ну и получил, соответственно,
test.rs:17:1: 22:2 error: reached the recursion limit during monomorphization
test.rs:17 fn genprod<T : Product>(x : T, y : T, n : int) -> int {
test.rs:18   match n {
test.rs:19     0 => x.product(y),
test.rs:20     _ => genprod(Cons {head : n+1, tail : x}, Cons {head : n*n+2, tail: y}, n-1)
test.rs:21   }
test.rs:22 }

невозможность писать «специализации» полиморфных функций.

Ну дык на том стоит ML — дженерик-функция пишется один раз, и работает со всеми возможными типами. И в Rust всё точно так же.

Другой вопрос, что:

1) Компилятор (не обязательно rust) в НЕКОТОРЫХ СЛУЧАЯХ использует специализированную функцию — скажем, GHC вообще этим знаменит, он буквально для каждой полиморфной функции старается нагенерить побольше специализаций. Но этот процесс никак не контролируется программистом и вообще снаружи не виден (кроме того факта, что некоторые функции работают чуть побыстрее).

2) Есть возможность (используя всякие фокусы типа overlapping instances) сделать иную реализацию тайпкласса. Типа

data T a = ...
class C x where ...
instance C (T a) where ...
instance C (T Int) where ...
В этом случае, если функции из тайпкласса C используются для типа T Int — то будет использоваться вторая реализация.

Однако, тут есть ловушка, которую не все понимают (и регулярно попадаются). Выбор инстанса производится на стадии компиляции. То есть, если использовать функцию из класса C в ситуации, когда конкретный тип неизвестен (и не может быть определён исходя из типов аргументов), то вне зависимости от того, окажется там на самом деле Int или что-то другое — выбрана будет общая реализация.

То есть, вот такой код:

{-# LANGUAGE ExistentialQuantification, FlexibleInstances, OverlappingInstances #-}
module Main where
data T a = T a
class C x where c :: x -> Int
instance C (T a) where c _ = 0
instance C (T Int) where c (T n) = n
data E = forall a. E a
test :: E -> Int
test (E a) = c (T a)
main =
  do print $ c $ T (4 :: Int)
     print $ test $ E (4 :: Int)
выведет
4
0

То есть, мы не можем утверждать, что для типа Int всегда работает специализированный вариант. Работает, но только в простых случаях.

Исходная версия Miguel, :

В своё время я об этом заводил баг — мне прямым текстом сказали, что мономорфизация в Rust by design и нефиг раскачивать лодку. Правда, это было несколько месяцев назад — но недавно мне в уши надули, что, якобы, появились настоящие дженерики, и я попробовал заново.

struct Nil;
struct Cons<T> {
  head : int,
  tail : T
}
trait Product {
  fn product(&self, other: Self) -> int;
}
impl Product for Nil {
  fn product(&self, _other: Nil) -> int {0}
}
impl<T : Product> Product for Cons<T> {
  fn product(&self, other: Cons<T>) -> int {
    self.head * other.head + self.tail.product(other.tail)
  }
}
fn genprod<T : Product>(x : T, y : T, n : int) -> int {
  match n {
    0 => x.product(y),
    _ => genprod(Cons {head : n+1, tail : x}, Cons {head : n*n+2, tail: y}, n-1)
  }
}
fn essence(n : int) -> int {genprod(Nil, Nil, n)}
fn main() {
  println!("{}", essence(5));
}
Ну и получил, соответственно,
test.rs:17:1: 22:2 error: reached the recursion limit during monomorphization
test.rs:17 fn genprod<T : Product>(x : T, y : T, n : int) -> int {
test.rs:18   match n {
test.rs:19     0 => x.product(y),
test.rs:20     _ => genprod(Cons {head : n+1, tail : x}, Cons {head : n*n+2, tail: y}, n-1)
test.rs:21   }
test.rs:22 }

невозможность писать «специализации» полиморфных функций.

Ну дык на том стоит ML — дженерик-функция пишется один раз, и работает со всеми возможными типами.

Другой вопрос, что:

1) Компилятор в НЕКОТОРЫХ СЛУЧАЯХ использует специализированную функцию — скажем, GHC вообще этим знаменит, он буквально для каждой полиморфной функции старается нагенерить побольше специализаций. Но этот процесс никак не контролируется программистом и вообще снаружи не виден (кроме того факта, что некоторые функции работают чуть побыстрее).

2) Есть возможность (используя всякие фокусы типа overlapping instances) сделать иную реализацию тайпкласса. Типа

data T a = ...
class C x where ...
instance C (T a) where ...
instance C (T Int) where ...
В этом случае, если функции из тайпкласса C используются для типа T Int — то будет использоваться вторая реализация.

Однако, тут есть ловушка, которую не все понимают (и регулярно попадаются). Выбор инстанса производится на стадии компиляции. То есть, если использовать функцию из класса C в ситуации, когда конкретный тип неизвестен (и не может быть определён исходя из типов аргументов), то вне зависимости от того, окажется там на самом деле Int или что-то другое — выбрана будет общая реализация.

То есть, вот такой код:

{-# LANGUAGE ExistentialQuantification, FlexibleInstances, OverlappingInstances #-}
module Main where
data T a = T a
class C x where c :: x -> Int
instance C (T a) where c _ = 0
instance C (T Int) where c (T n) = n
data E = forall a. E a
test :: E -> Int
test (E a) = c (T a)
main =
  do print $ c $ T (4 :: Int)
     print $ test $ E (4 :: Int)
выведет
4
0

То есть, мы не можем утверждать, что для типа Int всегда работает специализированный вариант. Работает, но только в простых случаях.