Comments 29
В связи с чем вопрос: можно ли выполнить 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 информацию о строгом типе теряет.
Например глубокое сравнение для 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
Так что рано или поздно сделают.
1. Функциональные методы на коллекциях (типа map, filter и т. д.) Iterable после каждого использования создают новый массив. Везде приходится прописывать сначала asSequence. Чем им не нравилась концепция «как в Scala» или хотя бы «как в C#» — без понятия. Очень бы хотелось посмотреть в невинные глазки этого «гения», что это придумал.
2. Жутко засорено глобальное пространство имён. Все эти listOf, println и пр. — все доступны всегда и везде без всяких import.
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 код, рассчитывающий на эту информацию будет несколько удивлен.
- Немножко громоздко, но можно привыкнуть. Я правильно понимаю, что вот для такого понадобятся дополнительные скобки?
Вот для такого на самом деле в текущей версии языка придется делать кое что покруче чем скобки:
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 лямбды — часто это работает, а если не работает приходится по старинке генерировать анонимный класс что громоздко.
Помимо того, что в них лучше контролируются типы, так еще и позволяют различать созданные с разной целью величины одного типа. А в типе String|String не поймешь, правая строка или левая.
Во первых, case class и data class как раз и нету :). Типа String|String быть не может — он сворачивается просто в String
Излишне это бывает, например, в Validation, когда комбинируются ошибки из разных источников. Может оказаться, что каждый второй метод требует специального ADT под тип ошибки, да еще и мапить ошибки постоянно. С объединениями же можно написать так:
Validated[Error1 | Error2, Result].
В Dotty собираются ввести nullable типы следующим образом: T? становится синонимом для T | Null. Это тоже весьма удобно и не влечет потери в производительности.
Но как основную причину появления объединений в Dotty я слышал то, что в некоторых случаях при попытке свернуть тип выражения (например 2 веток if) компилятор упирался в бесконечные или просто невыразимые типы, так что объединения упростили компилятор и дизайн языка.
union types
Подумалось, что причина появления языков типа PHP, где можно "вернуть любой тип из функции", это на самом деле желание вернуть не любой, а один из заранее заданного набора типов. Просто об этом не подумали или не захотели реализовать.
- Очень жаль, что и в Ceylon, и в Kotlin под деструктуризацией понимается только деструктуризация кортежей.
- Не совсем понятно, как связаны union types и перегрузка операторов.
В Scala, эквивалентный код будет выглядеть следующим образом:
Это неправда, идеоматический вариант:
val singleton: Tuple1[Long] = new Tuple1(1)
val singleton = (1)
new Tuple1 на практике не встречается.
Почему стоит полностью переходить на Ceylon или Kotlin (часть 2)