Pull to refresh

Comments 29

Судя по описанию, кортежи в ceylon ближе к HList, чем к Tuple.
В связи с чем вопрос: можно ли выполнить map по кортежу с сохранением информации о типах элементов?
Примерно так:
scala> (1, "str", 'sym) map elemToList
res1: (List[Int], List[String], List[Symbol]) = (List(1),List(str),List('sym))

Полный код по ссылке.

Нет, информация о типе не сохранится.


{String+} mapped = [1, 1.0, "2.3"].map((Integer|Float|String element) => element.string);

Результат Map — всегда Iterable. Iterable параметризуется одним типом. Здесь интереснее — в map приходит union тип, уже лямбда, передаваемая в map информацию о строгом типе теряет.

Жаль. Возможность возвращать разный тип результата в зависимости от типа аргумента в HList::map позволяет делать очень интересные вещи.
Например глубокое сравнение для case class:
Foo(2, "foo") delta Foo(8, "bar")
// 6 :: ("foo", "bar") :: HNil
На всякий случай, чтоб мы говорили об одном и том же.
Вот пример полиморфного метода: https://scalafiddle.io/sf/8Sq4LJv/0
(1, 3.14, "str") map increase
// (Int, Double, String) = (2,4.140000000000001,str + 1)

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


Отдельный метод, принимающий кортеж, у которого обязательства вернуть кортеж строго с такими же типами. Внутри обработчик, принимающий union type, обрабатывающий типы и инкрементящий. И при возврате проверка, что тип кортежа не изменился.


В Ceylon нет многого из того, что есть в Scala. В Kotlin аналогично :).

строго с такими же типами

Тут я все-таки опять не достаточно хороший пример привел. Тип результата не обязан совпадать с типом параметра. Совсем синтетический пример: https://scalafiddle.io/sf/apdsGEh/4.
Также есть zip на HList, возвращающий строго типизированный HList и многие другие методы.
Подобное требуется при метапрограммировании.

Осуществить подобное....


Можно попробовать использовать обычный map. Вернется условно Seq[Object]. И далее можно этот Seq[Object] преобразовать снова к Tuple уже конкретных типов. Но к сожалению тип не получится вывести автоматом, мне придется делать что то ручного каста.


Но вообще, фича запланирована:
https://github.com/ceylon/ceylon/issues/5838
Так что рано или поздно сделают.

Только вот в Kotlin есть 2 вещи, которые реально бесят.
1. Функциональные методы на коллекциях (типа map, filter и т. д.) Iterable после каждого использования создают новый массив. Везде приходится прописывать сначала asSequence. Чем им не нравилась концепция «как в Scala» или хотя бы «как в C#» — без понятия. Очень бы хотелось посмотреть в невинные глазки этого «гения», что это придумал.
2. Жутко засорено глобальное пространство имён. Все эти listOf, println и пр. — все доступны всегда и везде без всяких import.
UFO just landed and posted this here
Не знаю, как в Java, но в C# и Scala ничего не пишу. И там и там эти методы ленивые и вычисляются на потоке элементов, один за одним. И только в Kotlin выпендрились и материализуют всю коллекцию (а это выделение динамического буфера под все объекты, и копирование всех элементов в него — очень нехилые по прожорливости операции) сразу после каждого метода в цепочке.
UFO just landed and posted this here
Как раз у Kotlin свои Iterable/List/Collection/Sequence.
UFO just landed and posted this here
Хм. А если целевая платформа javascript?
UFO just landed and posted this here
Раз уж в статье идет сравнение со scala, я позволю себе развить эту тему:
18# Типы — объединения (union types)
В большинстве языков программирования функция может может возвратить значения строго одного типа.
Это если не учитывать наследование и ADT.
Для возвращения ошибок есть Validation
val p = Validated.catchNonFatal {
  Integer.parseInt("56")
}

Если же очень хочется именно объединения, то это потребует не намного больше кода:
type F = Boolean :+: Double :+: String :+: Null :+: List[Nothing] :+: CNil
def f(): F = {
  val rnd = util.Random.nextInt(5)
  rnd match {
    case 0 => Coproduct[F](false)
    case 1 => Coproduct[F](1.0)
    case 2 => Coproduct[F]("2")
    case 3 => Coproduct[F](null)
    case _ => Coproduct[F](List.empty)
  }
}

val v = f()

v.select[Double].foreach { d => println(s"Double $d") }
v.select[Int].foreach { i => println(s"Int $i") } // не скомпилируется, v не может быть Int

Кстати, в ceylon можно ли обработать весь тип объединения полиморфным методом с сохранением типов?
object headOption extends (List ~> Option) {
  def apply[T](l: List[T]) = l.headOption
}

val x = Coproduct[List[Int] :+: List[String] :+: CNil]("str" :: Nil)

val res: Option[Int] :+: Option[String] :+: CNil = x map headOption

res.select[Option[Int]].isEmpty
// true
res.select[Option[String]].nonEmpty
// true


19# Типы — пересечения (Intersection types)

Это не проблема тех пор, как придумали наследование:
trait CanRun {
    def run() = println("I am running")
}

trait CanSwim {
    def swim() = println("I am swimming")
}

trait CanFly {
    def fly() = println("I am flying")
}

case class Duck() extends CanRun with CanSwim with CanFly
case class Pigeon() extends CanRun with CanFly {}
case class Chicken() extends CanRun {}
case class Fish() extends CanSwim {}

def f(arg: CanFly with CanSwim) = {
    arg.fly(); 
    arg.swim();
}

f(Duck()); //OK Duck can swim and fly
f(Fish()); //ERROR = fish can swim only


20# Типы — перечисления (enumerated types)

Обычно это называется ADT (algebraic data type).
В scala тоже позволяет компилятору проверять полноту сопоставления с образцом.

23# Алиасы типов (Type aliases)
Или на класс, причем класс с конструктором:

Интересный синтаксис, но мне кажется удобнее сделать ссылку на весь компаньон, чтоб получить все методы:
type MyList[T] = List[T]
val MyList = List

val ml1: MyList[Int] = MyList.empty
val ml2: MyList[Int] = MyList("str")


21# Кортежи

Выше уже отметил, что лучшим аналогом в scala является HList.

22# Конструирование коллекций (for comprehensions)

Многомерное итерирование поддерживатеся?
val res = for {
 x <- 1 to 15 by 2
 if x % 3 == 0
 y <- 1 to 10 by 3
} yield x -> y
// Vector((3,1), (3,4), (3,7), (3,10), (9,1), (9,4), (9,7), (9,10), (15,1), (15,4), (15,7), (15,10))

Это только для коллекций или можно обобщить на произвольную монаду?

24# Улучшенные дженерики

Вообще хорошо, но нельзя забывать, что это влечет некоторые проблемы при взаимодействии с java. Например с java библиотеками для сериализации в JSON.

24# Метамодель

Звучит очень хорошо. Это полноценный механизм макросов как в scala? Можно при помощи него генерировать новые классы во время компиляции?

#25 Общий дизайн языка

Дискуссионный момент. Кому-то (например мне) может больше нравиться подход scala, где сделан упор на расширяемость языка, что позволяет делать такие сторонние библиотеки как shapeless, cats и другие, привносящие в язык новые концепции.
22# Конструирование коллекций (for comprehensions)
Многомерное итерирование поддерживатеся?

Эквивалентный код:


    value res = {for (x in (1:15).by(2)) 
                 if (x % 3 == 0) 
                 for (y in (1:10).by(3)) x -> y}

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


24# Метамодель
Звучит очень хорошо. Это полноценный механизм макросов как в scala? Можно при помощи него генерировать новые классы во время компиляции?

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


24# Улучшенные дженерики
Вообще хорошо, но нельзя забывать, что это влечет некоторые проблемы при взаимодействии с java. Например с java библиотеками для сериализации в JSON.

На самом деле в основном тут с большим потреблением памяти. Когда в коллекции с дженериками потребовалось пихать миллиарды значений, это съело примерно в 2 раза больше памяти чем на Java коллекциях. Потому пришлось быстренько эти места переписать на Java. Кстати, именно с библиотеками для сериализации именно с улучшенными дженериками проблем то и нет :). Там в самом языке есть хинт — ee режим. Если нужно сериализовать классы, и они помечены определенными аннотациями, соответствующая коллекция будет для Java библиотеки выгляжеть как Java коллекция, и сериализация пройдет. С сериализацией есть действительно проблемы, но в принципе жить можно. Самая очевидная — иерархические класслоадеры. Сериализовать без проблем. А десериализовать — проблема, так как класслоадер библиотеки сериализации не видит классы, вызывающие эту библиотеку. Но есть хинт, ключ --flat-classpath, в результате чего класслоадер будет один и поведение как в Java/


25 Общий дизайн языка

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

Спасибо, интересно.

22. Немножко громоздко, но можно привыкнуть. Я правильно понимаю, что вот для такого понадобятся дополнительные скобки?
val res = for {
 x <- 1 to 15 by 2
 z = x*x -1
 if z % 3 == 0
 y <- 1 to 10 by 3
} yield z -> y

Future же к коллекции не привести, или я не прав? Да и потеря типа не приятна.
Есть аналог async/await?
Там, на самом деле более сложный вопрос — как в for работать с Future[Validation[...]] — для асинхронной обработки результата. В scala для этого monad transformers.

24. Мне кажется больше проблем в десериализации generic типа — если экземпляр создан в java, то у него не будет метаинформации о конкретном типе с которым он создан. И ceylon код, рассчитывающий на эту информацию будет несколько удивлен.
  1. Немножко громоздко, но можно привыкнуть. Я правильно понимаю, что вот для такого понадобятся дополнительные скобки?

Вот для такого на самом деле в текущей версии языка придется делать кое что покруче чем скобки:


    value res = {for (x in (1..15).by(2))
                 for (z in {x * x - 1})
                 if (z % 3 == 0)
                 for (y in (1..10).by(3))
                 z->y};

То есть для z пришлось имитировать итерированием по последовательности из одного элемента. В будущем обещают let разрешить внутри for comprehensions, пока так.


Future по идее можно привести к коллекции из одного элемента. Примерно таким же способом, как я с z извратился.


async await планируют в будущем, но не знаю сколько лет еще ждать. Я пока вместо asinc await просто rxJava использую. Плюс у меня есть библиотечный async метод, который стартует Java поток. Возвращающий Observable. Соответственно внутри потока могу стартовать еще с помощью async другие потоки, и далее у Observable вызываю blockingFirst. Пока не появится async await в языке — живу вот так.


24

На самом деле Ceylon достаточно неплохо работает с Java коллекциями и в принчипе взаимодействие для программиста незаметное. Правда пока можно нарваться на багу взаимодействия с Java, в результате чего может не скомпилиться и придется искать обходные пути, фича новая и не все крайние условия пойманы. Касается баги в основном преобразования цейлоновских лямбд в Java лямбды — часто это работает, а если не работает приходится по старинке генерировать анонимный класс что громоздко.

Ни когда не мог понять, зачем нужны union types, если есть алгебраические типы (они же case class/data class).
Помимо того, что в них лучше контролируются типы, так еще и позволяют различать созданные с разной целью величины одного типа. А в типе String|String не поймешь, правая строка или левая.

Во первых, case class и data class как раз и нету :). Типа String|String быть не может — он сворачивается просто в String

Кстати, это отлично иллюстрирует, что использовать объединения в качестве Validation нельзя. Метод getStoredError будет иметь тип Error | Error.
Иногда создавать ADT либо излишне, либо невозможно.

Излишне это бывает, например, в Validation, когда комбинируются ошибки из разных источников. Может оказаться, что каждый второй метод требует специального ADT под тип ошибки, да еще и мапить ошибки постоянно. С объединениями же можно написать так:
Validated[Error1 | Error2, Result].

В Dotty собираются ввести nullable типы следующим образом: T? становится синонимом для T | Null. Это тоже весьма удобно и не влечет потери в производительности.

Но как основную причину появления объединений в Dotty я слышал то, что в некоторых случаях при попытке свернуть тип выражения (например 2 веток if) компилятор упирался в бесконечные или просто невыразимые типы, так что объединения упростили компилятор и дизайн языка.
UFO just landed and posted this here
Можно. Только компилятору будет сложнее отследить правильность использования с учетом вывода типов.
union types

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

  1. Очень жаль, что и в Ceylon, и в Kotlin под деструктуризацией понимается только деструктуризация кортежей.
  2. Не совсем понятно, как связаны union types и перегрузка операторов.
  3. В Scala, эквивалентный код будет выглядеть следующим образом:
    val singleton: Tuple1[Long] = new Tuple1(1)
    

    Это неправда, идеоматический вариант:
    val singleton = (1)
    

    new Tuple1 на практике не встречается.

1) Есть еще деструктуризация Entry, то есть пар ключ значение
2) Виноват, ошибся, имел в виду перегрузку методов или функций
Связано просто, пишем:


void overloaded(Integer|Float param) {
}

Вместо


void overloaded(Integer param) {}
void overloaded(Float param) {}
Sign up to leave a comment.

Articles

Change theme settings