juan_gandhi: (VP)
Juan-Carlos Gandhi ([personal profile] juan_gandhi) wrote2013-01-23 07:37 pm

and more on type classes in Scala

This part is not exactly a theory of algebraic theories, but anyway.

Imagine we have a parametric class (e.g. List[X]) for which we would like to define methods that would only work if X satisfies certain conditions. E.g. define sum if it is a numeric type, or define flatten if it is some kind of sequence (Traversable[Y] would be enough). How can we accomplish it in Scala?!

Suppose we want to declare flatten
trait List[X] {
// bla-bla-bla

  def flatten[X <% Iterable[Y]] { ... } // no way, we already mentioned X; this one would shadow the previous one
// OOPS!
}


The trick would be to have an implicit transformation handy that transforms X into Iterable[Y] for some Y; and define it so that if X is actually some kind of Iterable, the implicit would be available, and otherwise not available.

Where can we find such a general transformation? The only thing that comes up to mind is an identity:

implicit def itsme[Y, X <% Iterable[Y]](x: X): Iterable[Y] = x

This would be enough in this specific case... but then we would have to define another one for numeric types, and so on.

We can go generic, and write something like this:
abstract class SafeToCast[-S, +T] extends Function1[S, T]
implicit def canCast[X <% Y, Y]: SafeToCast[X, Y] = new SafeToCast[X,Y] { def apply(x:X) = x }


Now we can define flatten, like this:
class List[X] { ...
  def flatten[Y](implicit transformer: SafeToCast[X, Iterable[Y]]) { ... }
}


All the conditions are satisfied now. Contravariance in the first argument and covariance in the second argument ensure that a subtype can be cast to its supertype... to be more precise, an instance of supertype can be substituted by an instance of subtype.

The only thing is that we can try to make the code more readable, by refactoring.

Step 1. Use the trick in Scala that binary operations can be written in an infix way, even for types. E.g. you can declare val map: (String Map Int) - this is the same as Map[String, Int].

sealed abstract class SafeToCast[-S, +T] extends Function1[S, T]
implicit def canCast[X <% Y, Y]: (X SafeToCast Y) = new (X SafeToCast Y) { def apply(x: X) = a }
...

class List[X] { ...
  def flatten[Y](implicit transformer: X SafeToCast Iterable[Y]) { ... }
}


Step 2. Rename the method, giving it a more "type-relationship" name: SafeToCast -> <:<.

sealed abstract class <:<[-S, +T] extends Function1[S, T]
implicit def canCast[X <% Y, Y]: (X <:< Y) = new (X <:< Y) { def apply(x: X) = a }
...

class List[X] { ...
  def flatten[Y](implicit transformer: X <:< Iterable[Y]) { ... }
}


That's the trick.

(if you have questions, ask; if you have corrections, please tell me)

[identity profile] vit-r.livejournal.com 2013-01-24 12:04 pm (UTC)(link)
Я бы не создавал случая, когда вещи называются одним и тем же именем, а ведут себя принципиально по-разному. И с неявными преобразованиями я бы не игрался. Тайные знания как правило ни к чему хорошему не приводят.

[identity profile] xeno-by.livejournal.com 2013-01-24 12:30 pm (UTC)(link)
Везде есть компромиссы. В Сишарпе, например, Filter/Select/итд возвращают IEnumerable вне зависимости от типа коллекции. У нас в Скале для листа вернется лист, для вектора - вектор, для мапы - мап. Мне неочевидно, что увеличение подкапотной сложности не оправдывается достигаемым удобством.

[identity profile] vit-r.livejournal.com 2013-01-24 12:39 pm (UTC)(link)
Одному надо в список добавить элемент со строкой, другому - сосчитать сумму. При явных преобразованиях они не договорятся. При неявных - намутят где-то в глубинах и создадут фигню.

Сишарп я бы за пример не брал.

[identity profile] xeno-by.livejournal.com 2013-01-24 12:45 pm (UTC)(link)
Implicit search это, в основном, про type-level пролог, а не про неявные преобразования. К счастью, implicit conversions все больше и больше становятся дурным тоном в сообществе Скалы.

Вот как, например, было в посте:

implicit def canCast[X]: SafeToCast[X, X] = new SafeToCast[A,A] { def apply(a: A) = a }

Здесь implicit определение выражает то, что можно скастовать любой тип сам в себя. Никто никого не преобразовывает, просто на основе фактов, указанных таким образом, тайпчекер выдает ответ: "да, подходит" или "нет, не подходит".

[identity profile] sassa-nf.livejournal.com 2013-01-28 07:06 pm (UTC)(link)
Там на рассылке интересный момент выясняется.

В хаскелле экзистенциальные типы и rank-N полиморфизм выражаются с помощью (forall blah . P blah => ...), что на деле выражается в виде функции с неявным параметром, который в точке вызова конструируется неявно же, и является ничем иным как словарём функций.

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


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


ага, а чё я, собственно, это всё говорю. Если имплиситы всё более дурным тоном считается, то что ему на замену хорошим тоном считают?
Edited 2013-01-28 19:08 (UTC)

[identity profile] ivan-gandhi.livejournal.com 2013-01-28 07:34 pm (UTC)(link)
Я уверен, что имплиситы считают дурным тоном те, кто их готовить не умеет. Это примерно как жалобы тупых на джавные дженерики (есть ещё правильные жалобы, но тупые джавщики даже слов таких не знают, как variance and higher order).

[identity profile] xeno-by.livejournal.com 2013-01-28 07:43 pm (UTC)(link)
Я имел ввиду исключительно implicit conversions. Мы в Scala compiler team всеми силами их избегаем.
Edited 2013-01-28 19:44 (UTC)

[identity profile] ivan-gandhi.livejournal.com 2013-01-28 10:01 pm (UTC)(link)
Может быть, в компиляторе они и моветон; а в жизни написать sleep(10 seconds) - милое дело.

[identity profile] xeno-by.livejournal.com 2013-01-28 10:23 pm (UTC)(link)
Это implicit classes. Их тоже можно. Их даже SIP-18 разрешает :)

Я скорее имею ввиду няшечки типа неявных преобразований из String в TermName или TypeName для удобства вызова функций, которые принимают имена. В итоге потом местами очень затруднительно понять, где имена, где строки, а где включился умный конвершн и, например, сломал сравнение на равенство. И живучие же эти конверсии - до сих пор еще, вроде бы, не отовсюду из компилятора вычистили.

[identity profile] sassa-nf.livejournal.com 2013-01-28 08:35 pm (UTC)(link)
Ну мне из эстетических соображений forSome {...} кажется то ли приятнее, то ли понятнее.

Более явно, что ограничение является compile-time, что ли.

[identity profile] http://users.livejournal.com/_windwalker_/ 2013-01-25 07:49 pm (UTC)(link)
ну от чего-же. Для обладателя тайных знаний - к job security, пока не найдут 100500 индусских программистов способных расковырять тайные знания.