16 месяцев функционального программирования

Предлагаю читателям «Хабрахабра» перевод статьи «16 Months of Functional Programming». Все мои замечания будут выделены курсивом.

В этой статье я хочу поделиться с вами моим опытом в функциональном программировании. Я чувствую, что в целом за прошедшие 16 месяцев стал лучше разбираться в информатике и компьютерах, чем за предыдущие 10 лет и всё это благодаря моему погружению в Scala и мир функционального программирования. Причина по которой функциональное программирование побуждает вас к постоянному развитию заключается в том, что каждую задачу необходимо переосмысливать заново. Порой невозможно поверить в то, что большинство стандартных задач могут быть решены иным путём и — бум! — функциональный подход предлагает лучшее решение и это шокирует.

Так что же случилось со мной за эти 16 месяцев? Закрыв свой стартап, я стал подыскивать интересный проект для работы. Получил временную подработку консультантом в 2lemetry, что позднее вылилось в полный рабочий день с погружением в функциональное программирование, работу с MQTT брокерами и распределёнными системами. Во время работы в 2lemetry Scala оказала на меня сильнейшее влияние. Приходилось отбрасывать свои представления о программировании и обучаться всему с нуля.

В этой статье я хочу рассказать о некоторых концепциях функционального программирования, которые меня впечатлили. Моя цель — зажечь искру в умах программистов, которые уже имеют опыт работы с такими языками как Java, Ruby и Python. Не беспокойтесь о том, что некоторый код на Scala, который я привожу в этой статье, может быть вам непонятен. Цель этого кода — продемонстрировать концепции функционального программирования в общих чертах. Так же стоит упомянуть, что Scala — это не чистый функциональный язык, т.е. некоторые вещи могут показатся неудобными при смешивании их с ООП. Я постараюсь указывать на такие моменты.

Содержание:

  • Неизменяемое состояние
  • Функции
  • Тип Option и сопоставление с образцом
  • Однострочники и for-генераторы
  • Система типов
  • Ленивые вычисления и бесконечные структуры данных
  • Что дальше?

Неизменяемое состояние


Мой первый опыт работы со Scala был достаточно типичным: я начал работать над большим проектом с ощущением того, что Scala — это как Java с некоторыми крутыми дополнениями и некоторыми возможностями из Ruby. Как же сильно я ошибался! Мой код содержал изменяемое состояние везде, где это только возможно и я не понимал для чего могут пригодиться неизменяемые списки и переменные. Как изменить значения в неизменяемых списках? Как изменить значения в неизменяемых отображениях (map)? Как работать с неизменяемым состоянием в циклах?

Для демонстрации преимущества от неизменямого состояния я покажу две версии одной и той же программы, одна на Java, другая на Scala. Следующий код на Java фильтрует список пользователей по флагу активности, сортирует его по ID, затем получает список имён у этих отфильтрованных пользователей:

public class User {
  private final int id;
  private final String firstName;
  private final String lastName;
  private final Boolean active;

  // Для краткости я пропустил конструкторы, геттеры и сеттеры
}

public static List<String> activeById(List<User> us) {
   List<User> users = new ArrayList<User>();

   for (User u: us) {
     if (u.getActive()) users.add(u);
   }

   Collections.sort(users, new Comparator<User>() {
      public int compare(User a, User b) {
        return a.getId() - b.getId();
      }
   });

   List<String> finalUsers = new ArrayList<String>();

   for (User u: users) {
     finalUsers.add(u.getLastname());
   }

   return finalUsers;
}

List<User> inputUsers = new ArrayList<User>();
inputUsers.add(new User(11, "Nick", "Smith", false));
inputUsers.add(new User(89, "Ken", "Pratt", true));
inputUsers.add(new User(23, "Jack", "Sparrow", true));

List<User> activeUsersById = activeById(inputUsers)

Автору оригинальной статьи в комментариях указали, что на Java 8 этот код был бы проще. Он ответил, что его задача показать общепринятый императивный подход при работе с данными.

Это типичный код для Java до 8-ой версии: каждая коллекция изменяется некоторым набором действий. Кроме того, весь код несколько многословен, в каждой части кода activeById вы говорите компьютеру что вы хотите, чтобы он сделал с данными вместо того, чтобы описать как данные должны быть обработаны от начала и до конца. Вот та же самая программа написанная в функциональном стиле:

case class User(id: Int, firstname: String, lastname: String, active: Boolean)

def activeById(us: Seq[User]) = us.filter(_.active).sortBy(_.id).map(_.lastname)

val activeUsersById = activeById(Seq(
  User(11, "Nick", "Smith", false),
  User(89, "Ken", "Pratt", true),
  User(23, "Jack", "Sparrow", true)
))

В отличие от примера на Java, код приведён полностью, т.е. можно его запускать в таком виде и он будет работать.

Код выглядит чище и короче по сравнению с императивным подходом, потому что здесь нет состояния которое надо отслеживать. Функция activeById принимает один аргумент (список пользователей) и пропускает его через цепочку функций, которые являются часть языка. Важно отметить, что функции filter, sortBy и map выбраны не случайно. Эти функции отлично описаны и изучены приверженцами функционального программирования.

Рубисты должны были заметить, что этот пример кода очень похож на то, что они пишут на Ruby. Да, этот код может выглядеть похоже, но механизм неизменного состояния, лежащий в основе, сильно отличается. Проблема Ruby в том, что он не поддерживает неизменное состояние. Каждая переменная и структура данных потенциально может быть изменена и это приводит к тому, что нельзя ничему доверять. В Scala есть vals (read-only переменные) и неизменяемые коллекции, которые действительно неизменны.

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

Функции


Функциональное программирование — это о функциях (сюрприз?). Существуют различные виды функций и техник функциональной композиции, которые используются в функциональном программировании.

Чистые функции являются одним из столпов функционального программирования. Чистая функция — это функция, которая зависит только от её входных параметров. Она возвращает результат без изменения внешнего состояния. Функции sin(x: Double) или md5(s: String) отличные примеры чистых функций, которые зависят только от входных параметров и всегда возвращают ожидаемый результат, т.к. они не полагаются на состояние окружающего мира. Эти факторы делают чистые функции легко тестируемыми и менее склонным к багам.

Очевидно, что не все задачи могут быть реализованы с использованием чистых функций. Например, операции ввода/вывода, логирования, чтения и записи в БД и т.д. В функциональном программировании существуют модели и абстракции, которые позволяют реализовать эти нечистые абстракции в чистом виде, что приводит к более чистому и компонуемому коду.

Функции в Scala являются объектами первого класса. Это означает, что не только методы класса могут быть объявлены и вызваны. Кроме того, они могу использоваться как самостоятельный тип данных. Вы можете передавать функцию в другую функцию и возвращать из функции другую функцию. Вы можете сохранять функцию в переменную или в структуру данных. Вы можете работать с ней, как с литералом без того, чтобы как-то её называть (лямбда-функция). Пример:

val ints = Seq(1, 2, 3, 4, 5)

ints.filter(n => n % 2 == 0)

Посмотрите на n => n % 2 == 0, полная форма (n) => { n % 2 == 0 }, это функция без имени (лямбда-функция), которая проверяет, является ли число чётным. Вы можете передавать лямбда-функции как аргумент другой функции или использовать её как возвращаемое значение.

Функции могут быть вложенными в другие функции. Это полезная возможность, когда вам понадобится рекурсивно вызывать подпрограмму (subroutines), которую вы не хотите помещать в область видимости выходящую за пределы вашей функции.

def toList: List[A] = {
  @annotation.tailrec
  def go(s: Stream[A], l: List[A]): List[A] = s match {
    case Empty => l
    case Cons(h, t) => go(t(), h() +: l)
  }

  go(this, List[A]()).reverse
}

В этом примере мы не хотели бы, чтобы функция go была доступна в той же области видимости, что и функция toList, потому что её реализация необходима только для функции toList и в области видимости, в которой находится функция toList, может оказаться другая функция с названием go.

Каррирование и частичное применение функции — это чисто математическая концепция, которая прекрасно применяется в функциональных языках. Это позволяет нам сохранять частично вызванные функции в переменные и передавать их в другие функции.

trait Resource
case class User(id: Int) extends Resource
case class Record()
case class FormattedRecord()

def loadRecordsFor(r: Resource, since: Long): List[Record] = ???

def formatRecords(f: Long => List[Record], since: Long): List[FormattedRecord] = ???

val userRecordsLoader = loadRecordsFor(User(36), _: Long)

formatRecords(userRecordsLoader, System.currentTimeMillis - 86400000)

??? — означает объявление без определения

В этом примере у нас есть две шаблонные функции loadRecordsFor и formatRecords. Мы частично применили loadRecordsFor для некоторого пользователя и сохранили результат в переменную userRecordsLoader. Затем вызываем formatRecords с параметром userRecordsLoader, т.к. эта переменная соответствует сигнатуре функции formatRecords (т.е. вызываем функцию formatRecords с первым аргументом — частично применённой функцией). Этот вид функциональной композиции достаточно удобен во многих ситуациях и делает код менее жёстким.

Этот пример не очень показательный. Считаю, что тему каррирования автор не раскрыл.

Тип Option и сопоставление с образцом


Тип данных Option — это абстракция, которая представляет опциональные значения. Может показаться, что он не особо необходим, но в повседневной работе это черезвычайно мощный механизм для представления null, empty, битых объектов и переменных значений.

Тип Option — это контейнер, который содержит значение определённого типа, представляемого как Some[T] или ничего не содержащего, представляемого как None. Применение этого типа в нашем коде позволяет забыть о бесчисленных случаях возникновения исключений при обращении к null-указателю (null pointer exceptions) или какую-либо несовместимость типов, всякий раз, когда извлекаются null-значения.

Давайте рассмотрим пример:

case class Project(id: String, name: String, priority: Int, description: Option[String])

object ProjectsAccessor {
  find(id: String): Option[Project] = ???
}

val project = ProjectsAccessor.find(123)

Здесь мы пытаемся получить запись о проекте из БД, но мы не знаем существует ли проект с таким ID. Вместо того, чтобы возвращать null или выбрасывать исключение, мы возвращаем Some[Project] или None, т.к. при объявлении метода find мы указали тип возвращаемого значения как Option[Project].

Контейнерные типы позволяют нам использовать другой мощный инструмент — сопоставление с образцом (pattern matching). Сопоставление с образцом — это подход к обработке данных на основе их структуры. Eсли мы захотим обработать результат вызова метода find из примера выше и получить название проекта мы можем сделать что-то вроде:

ProjectsAccessor.find(123) match {
  case Some(p) => p.name
  case None => ""
}

Мы сопоставляем с образцом результат работы метода find для проверки существования проекта. Если он существует, то возвращается его название, иначе возвращаем пустую строку. На первый взгляд, это может выглядеть как оператор switch-case в Java, но на самом деле они сильно различаются. При сопоставлении с образцом вы можете добавлять нетривиальную логику в шаблоны:

ProjectsAccessor.find(123) match {
  case Some(p) if 1 until 5 contains p.priority => p.name
  case Some(p) if name == "Default Project" => p.name
  case Some(p) => None
  case None => ""
}

Так же вы можете сопоставлять результат на основе текущей структуры объекта:

def descriptionWrapper(p: Project) = p match {
  case Project(_, _, _, None) => "No description."
  case Project(id, _, _, Some(d)) => s"Project $id's description: $d"
}

Этот подход к обработке сложных логических конструкций более компактный и прямолинейный по сравнению с if-операторами и громоздким switch-case.

Однострочники (One-Liners) и for-генераторы


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

case class Participant(name: String, score: Int, active: Boolean)

val ps = Seq(Participant("Jack", 34, true), Participant("Tom", 51, true),
             Participant("Bob", 90, false))

ps.filter(_.score < 50).filter(_.active).map(_.copy(active = false))

В этом однострочнике мы собрали всех подписчиков чей результат меньше 50 и которые до сих пор активны, затем мы изменили статус у выбранных подписчиков на false. В конечном итоге мы получили List(Participant(Jack, 34, false)). Есть достаточно большое кол-во ситуаций в которых подобные однострочники сохраняют программистам время и резко сокращают количество кода.

Если однострочник становится слишком непрозрачным, то его всегда можно разбить с помощью for-генератора. Пример выше можно переписать в эквивалентное выражение:

for {
  loser <- ps if loser.score < 50
  activeLoser <- Some(loser) if activeLoser.active
  deactivatedLoser <- Some(activeLoser.copy(active = false))
} yield deactivatedLoser

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

В Scala for-генератор – это синтаксический сахар для цепочки функций.

Система типов


После программирования на Ruby, строгая типизация в Scala ощущалась как бремя, потому что я использовал её как джавист. Я добавлял подробное описание типов везде и не использовал обобщённые (generic) функции. Излишне сказать, что это был неправильный подход к работе. Некоторые функциональные языки обладают продвинутой системой типов с такими свойствами, которые не используются программистами на популярных языках. Эти свойства позволяют коду быть более гибким и компонуемым. Давайте пройдёмся по некоторым из них.

Вывод типов является способностью компилятора догадаться о типе выражения без явного указания его программистом. В некторых случаях Scala не может догадаться о типе без подсказки. Давайте посмотрим на пример:

// Вы всегда должны указывать типы передаваемых в метод аргументов
def nameStartsWith(ns: Seq[String], n: String): Seq[String] =
  // Scala не может вывести тип для обобщённых коллекций, т.е. вы не можете сказать Seq.empty
  ns.foldLeft(Seq.empty[String]) {
    // Но в лямбда-функции не требуется указыать тип явно
    (l, r) => if(r.startsWith(n)) r +: l else l
  }

// Вывод типов хорошо работает при создании списков
val names = Seq("Bob", "Alice", "Ann")

nameStartsWith(names, "A") // возвратит List(Ann, Alice)

Этот пример показывает обе стороны вывода типов в Scala: вы обязаны явно указывать типы, но в некоторых случаях, вроде того, когда вы передаёте лямбда-функцию в качестве аргумента (l, r) => ..., типы можно опускать. В чистых функциональных языках таких, как Haskell, вам вряд ли когда-либо придётся указывать тип в программах (спорное утверждение). Компилятор достаточно умён, чтобы вывести их.

Cтоит отметить, что при объявлении функции не обязательно было указывать тип возвращаемого значения, компилятор Scala выведет его сам.

Ограничение типов — это ещё одна важная концепция в функциональном программировании. В общем случае это означает, что вы можете указать иерархию класса при объявлении обобщенного типа. В Java вы можете использовать обобщения для определния типа во время исполнения и по-прежнему считать свой код типобезопасным. Для примера, чтобы объявить обобщённый список элементов некоторого типа вы должны использовать интерфейс (код на Java): public interface MyList<T>. Если вы хотите объявить список, скажем, содержащий отображения (Map), но при этом вы не знаете какая реализация отображений будет использована, то вам необходимо указать верхнюю границу для типа в вашем интерфейсе public interface MyList<T extends Map>. Теперь вы можете использовать список заполненный такими типами как Hashtable, LinkedHashMap, HashMap и TreeMap или другими словами всех потомков интерфейса Map. При этом никакой другой тип не может быть использован, т.к. обозначена верхняя граница. Вот пример использования верхней границы в Scala:

def convertToInts[T <: AnyVal](es: Seq[T], f: T => Int): Seq[Int] = {
  es.map(f(_))
}

AnyVal является родителем таких классов как Double, Float, Int и многих других (скалярных типов). В этой функции мы просто объявили, что тип T будет потомком AnyVal. Так же можно указать нижнюю границу типа, вроде [T >: Int] это будет соответствовать родителям типа Int. Вы так же можете смешивать границы типов для различных обобщений в сигнатуре функции: [T >: Int <: Any].

Одно из важных свойств продвинутой системы типов является ковариантность и контравариантность. В Java, если у вас есть class List<T>, List<Object> и List<String>, то они будут не связанны или инвариантны. С другой стороны существуют ковариантные массивы, т.е. String[] это подтип типа Object[]. Так как массивы являются изменяемыми в некоторых случаях вы можете получить исключение ArrayStoreException в рантайме. В Scala массивы инвариантны по-умолчанию и неизменяемые коллекции (или контейнерные типы) ковариантны [+A]. Так как они неизменяемые все потенциальные ошибки будут обнаружены во время компиляции, а не в рантайме. Другая возможность — указать контейнер контрвариантным [-A]. Контрвариантность означает то, что контейнер с родительским типом является подтипом контейнера с дочерним типом. Вот как это работает:

case class InvariantContainer[A]()
case class CovariantContainer[+A]()
case class ContravariantContainer[-A]()

class Person
class User extends Person
class Admin extends User

val inv1: InvariantContainer[User] = InvariantContainer[User] // works
val inv2: InvariantContainer[User] = InvariantContainer[Admin] // doesn't work
val cov1: CovariantContainer[User] = CovariantContainer[User] // works
val cov2: CovariantContainer[User] = CovariantContainer[Admin] // works
val con1: ContravariantContainer[User] = ContravariantContainer[User] // works
val con2: ContravariantContainer[User] = ContravariantContainer[Admin] // doesn't work
val con3: ContravariantContainer[User] = ContravariantContainer[Person] // works

Ковариантность и контрвариантность широко используется в реализации коллекций и хотросплетениях при указании типа функции.

Последняя продвинутая возможность типов, которую я хотел бы затронуть, это границы вида (view bounds). Предположим, что вам нужно выполнить операции над числами, но некоторые из них представлены в виде строк (т.е. «123» или 123). Как вы сделаете это? В простом случае, как этот, вы можете сконвертировать строки в числа вручную или, в более сложных случаях, написать собственный конвертер, а затем явно вызывать его для конвертации данных из одного типа в другой. В слаботипизированных языках, таких как Ruby, интерпретатор будет динамически конвертировать строки в числа. Возможно, вы будете удивлены тем, что в Scala есть способ реализовать подобное поведение без потери типобезопасности.

Насколько я понимаю, автор ошибается с Ruby, например, такой код работать не будет [10, «20»].min. В данном случае корректнее было бы привести в пример PHP.

Чтобы это заработало в Scala (давайте используем стандартную функцию math.min() для примера) всё что необходимо сделать — это определить неявный конвертер для ваших типов:

implicit def strToInt(x: String) = x.toInt

math.min("10", 1)

Тут Scala будет искать неявные преобразования из строки в число. После нахождения функции strToInt, основанной на её сигнатуре, она будет применена ко всем строкам переданным в math.min без явного вызова strToInt. Если вы не объявили неявную конвертацию, то компилятор выкинет исключение. А если объявили и попытаетесь передать строку, которая не является числом, то получите исключение в рантайме.

Что если мы хотим написать магическую функцию, которая найдёт неявную конвертацию для себя самой? Это очень просто! Всё что вам нужно — это объявить границу вида, которая укажет компилятору производить поиск неявной конвертации:

implicit def strToInt(x: String) = x.toInt

def halfIt[A <% Int](x: A) = x / 2

halfIt("20")

Результатом вызова halfIt(«20») будет 10 как и ожидалось. Граница вида [A <% Int] ожидает Int или всё, что может рассматриваться как Int. В нашем случае это строка, которая может быть неявно преобразованна в число.

Ленивые вычисления и бесконечные структуры данных


Концепция ленивых вычислений непосредственно не существует в нефункциональных языках, но её легко уловить. Посмотрите на обычное условие:

def expensiveOperation() = ???
val a = "foo"
val b = "foo"

if ((a == b) || expensiveOperation()) true else false

В большинстве императивных языков оператор || вычисляет предикаты (a == b) и expensiveOperation() лениво, т.е. expensiveOperation() не будет вызван до тех пор пока if (a == b) равен true (т.е. до тех пор, пока a равно b). И expensiveOperation() будет вызван если if (a == b) вернёт false. Ленивые вычисления в Scala позволяют определить подобное поведение в разных конекстах.

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

case class Order(name: String, price: Int)

case class User(id: Int, orders: Seq[Order]) {
  lazy val cheapOrders: Seq[Order] = orders.filter(_.price < 50)
  lazy val expensiveOrders: Seq[Order] = orders.filter(_.price >= 50)
}

В этом примере у нас есть case class для представления пользователя, который содержит в себе списки заказов. В отличие от обычных свойств, cheapOrders и expensiveOrders не будут вычислены в момент инициализации класса (т.е. в момент создания объекта из класса). Они вычисляются, когда мы обратимся к ним напрямую. Почему бы не использовать метод? Проблема в том, что у нас могут быть дорогостоящие вычисления или каждый раз происходить обращения к БД, когда мы вызываем метод. Ленивые переменные сохраняют свой результат при первом обращении, что может привести к эффективным оптимизациям в некоторых случаях.

В других языках используют технику мемоизации, что можно также рассматривать как ленвые вычисления.

Другой пример отложенных вычислений — это передача аргументов по имени (call-by-name). Обычно, аргументы функции вычисляются перед тем, как они будут в неё переданы. Тем не менее, в некоторых случаях бывает полезно вычислять переданный аргумент отложено тогда, когда это потребуется.

trait Person
case class User() extends Person
case class Admin() extends Person

def loadAdminsOrUsers(needAdmins: => Boolean, loadAdmins: => Seq[Admin],
                      loadUsers: => Seq[User]): Seq[Person] = {
  if (needAdmins) loadAdmins
  else loadUsers
}

Здесь у нас три параметра by-name с потенцаильно дорогостоящими операциями обращения к БД. Мы не хотели бы, чтобы все они были выполнены до того, как попадут в наш метод. Стрелка => означает, что мы передаём внутрь саму функцию, а не возвращаемое ею значение. Теперь мы можем вызывать её когда нам это потребуется.

Ленивые вычисления и параметры by-name используются для реализации одной из мощнейших конструкций функционального программирования — бесконечных структур данных. В императивном программировании все ваши структуры данных имеют определённый размер, что на самом деле отлично для большинства случаев, но иногда неизвестно какого размера будет структура данных. С отложенными вычислениями есть возможность объявить структуру данных в общем виде без её полного содержимого до тех пор, пока оно вам не потребуется.

Всё это хорошо звучит в теории, но будет ли это работать? Давайте воспользуемся бесконечной структурой данных, называемой stream для генерации простых чисел. В Java для генерации простых чисел можно написать функцию, которая генерировала бы простые числа до некоторого предела. Затем вы могли бы вызвать эту функцию для генерации списка из N простых чисел и передать его куда-нибудь. Если вам понадобится другой список простых чисел, то вам придётся пересчитать свой список с нуля. В Scala мы можем сделать что-то вроде:

val primes: Stream[Int] = {
  def generatePrimes (s: Stream[Int]): Stream[Int] =
    s.head #:: generatePrimes(s.tail filter (_ % s.head != 0))

  generatePrimes(Stream.from(2))
}

Синтаксис может показаться вам непонятным, но в данном случае это не имеет значения. Главное то, что вы можете сделать с этой структурой данных. Скажем, что вам необходимо получить первые 5 простых чисел, больших 100. Это легко, используя наш stream:

primes.filter(_ > 100).take(5).toList

Эта функциональная цепочка вернёт List(101, 103, 107, 109, 113) — как и ожидалось. Крутая вещь заключается в том, что вы можете передавать primes в любые другие функции без необходимости её всякий раз собирать заново. Так же вы можете применять цепочки функций (как filter в примере выше) и передавать уже это выражение без генерации промежуточного результата. Это позволяет нам компоновать абстракции и лепить программы как из пластилина.

Что дальше?


Надеюсь, я заинтересовал вас функциональным программированием. Для меня писать об этом было очень увлекательно. Я признаю, что ещё многое предстоит изучить и усвоить в этой области. Моя цель — начать глубже копать теорию функционального программирования, такую как типизированное лямбда-исчисление, теорию типов и теорию категорий. Я так же хочу изучить Haskell — чистый функциональный язык программирования, и надеюсь что это принесёт плоды.

Всем дочитавшим выражаю благодарность!
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 97
  • +8
    пожалуй лучший и самый содержательный гайд по Scala на хабре
    • +2
      Еще есть очень интересный язык от компании jetbrains — kotlinlang.org. Таже scala только проще в освоении.
      • +6
        kotlin'у до скалы ой как далеко
        • +2
          Kotlin не совсем корректно сравнивать со Scala, скорее с Java/C#, и он их делает :)
          • +2
            ох, я бы Java с C# в один ряд не ставил)
            • 0
              Не могу не согласиться :(
              • 0
                Шарп делает Джаву? Я не в курсе просто :(
                • 0
                  У нас один разработчик волею судеб был вынужден пересесть на год с C#/WPF на Java/Android. Рыдает и бьётся головой о стену. Говорит, что Java жутко убогая по сравнению с C#. Хоть enterprise-либы лучше.

                  Это чисто его мнение, а не моё! Я придерживаюсь нейтралитета и не верю, что там всё так плохо.
                  • +3
                    Это часто так бывает, когда человека вырывают из зоны комфорта.
                    • 0
                      За год можно было привыкнуть… Вчера опять был недоволен. Мол, generic-и в Java ненастоящие! И оператора typeof нет. Я, правда, не совсем уловил, что именно ему так не понравилось.
                      • 0
                        Generic-и стираются при компиляции, List во время выполнения это просто List
                        Вообще, в Java много «особенностей», к которым после более человечных языков привыкать не хочется
                      • +3
                        Когда пересаживают с нормального кресла на треногий табурет — возмущение естественно)
                        • 0
                          Зато трёхногий может стоять почти на любой поверхности ;)
                          • 0
                            Под JVM много стульев языков, дайте хотя-бы с колёсиками)
            • 0
              Вполне возможно, что есть еще сто интереснейших языков, но программистам платят не за интересность языка и не за легкость в его освоении, а за то, чтобы программа работу работала. Причем не дольше определенного времени и не потребляя больше определенных ресурсов компьютера. Поэтому Scala, как промышленно востребованный язык (то есть, специалисты по нему требуются во многие проекты), имеет громадное преимущество перед другими «интересными» языками.

              В связи с этим, кстати, хотелось бы услышать от работающих на Scala, насколько его красивые и изящные конструкции работают медленнее и потребляют больше памяти, чем грубые, длинные императивные алгоритмы? Или столько же? То есть, пока не очень понятен круг задач, где его можно использовать. Всякие абстрактные научные исследования и прочие курсовые работы сразу оставляем за бортом. Интересует коммерческое использование в реальных проектах.

              Ну и напоследок, «конкурентные программы» — это ж надо так перевести! В-)
              • +1
                Я совершенно не понимаю аргумента:

                «У этого языка код медленнее => Этот язык нельзя использовать в тех же целях».

                Это чисто логическая ошибка, ибо из одного другое не следует. Допустим, при идеальных условиях код на Java будет быстрее кода на Scala. Тогда конечно лучше Java. Но идеального кода не бывает — подавляющее большинство систем написаны не самым оптимальным образом, и язык здесь второстепенен. Программы просто не используют все доступные ресурсы. Так какая же разница, будет ли код на Java не использовать эти ресурсы, или код на Scala? Скорость работы программ не может быть критерием выбора языка, пока не доказано, что она напрямую зависит от этого языка.
                • 0
                  «У этого языка код медленнее => Этот язык нельзя использовать в тех же целях».

                  Покажите пальцем, где я такое сказал. Не покажете, потому что я не говорил. А сказал очень четко: «не дольше определенного времени».

                  Скорость работы программ не может быть критерием выбора языка, пока не доказано, что она напрямую зависит от этого языка.

                  Вот я и просил знающих рассказать о тестах для одних и тех же алгоритмов/систем/и т.п.
                  • 0
                    Тесты не покажут ничего полезного. Это не главный критерий выбора языка в современном мире.

                    Если бы было так, то для линейных алгоритмов мы бы до сих пор использовали код на ассемблере с хитрыми хаками, из-за которых алгоритм работал бы в n раз быстрее. Эти тесты слишком расплывчаты, и к ним обычно возникает очень много претензий. Конечно же алгоритм будет работать с разной скоростью, написанный разными людьми на разных языках в разных окружениях. И реализация может быть абсолютно разной. Производительность конкретного кода — необъективный параметр. Программа не будет состоять только из этого алгоритма, в ней есть еще много других компонентов, производительность которых никому не интересна. Но очень интересно, чтобы эти компоненты работали корректно, имели простой дизайн, быстро писались и легко поддерживались. Старые технологии уже не могут удовлетворить этим требованиям в полной мере — отсюда и движение в сторону ФП.

                    В конце концов, мультиязыковые проекты сейчас — вовсе не редкость. Если требуется, чтобы какой-то алгоритм работал быстро, и известно, что на языке А этого добиться проще всего — можно сделать интероп из другого языка в код на А, но остальные задачи решать наиболее пригодным способом.

                    И мне не понятно как вообще можно отрицать, что ваш абзац «В связи с этим, кстати...» может быть сведен к цепочке «У этого языка код медленнее => Этот язык нельзя использовать в тех же целях».
                    • 0
                      Тесты много чего покажут. И это часто очень существенный критерий для реальных проектов. В современном мире, именно в нем.

                      Код на ассемблере очень трудно писать и поддерживать, человекодень — тоже ресурс. Да и современные компиляторы (с С++, например) зачастую генерят более эффективный код.

                      Однако, никто не будет писать модель погоды на Python или Scala, как и не будет на них писать ядро ОС или систему торгов на бирже, или CAD, или 3D-игрушку, или фото/видеоредактор, базу данных, сервер сообщений и т.д. и т.п. Для всего есть своя область применения.
                      • +1
                        Соглашусь только про 3D игры. И то я бы взял в теперешнее время не С++, а что-нибудь из ряда Go, Rust.

                        А все остальное — запросто.

                        ejabberd — написан на Erlang.
                        erlyvideo — видеостриминговая система, Erlang. habrahabr.ru/post/114560/
                        backend в Twitter — написан на Scala. www.slideshare.net/al3x/the-how-and-why-of-scala-at-twitter
                        В AutoCad когда-то был встроен AutoLisp.
                        Есть кое-какие разработки ОС на Haskell: stackoverflow.com/questions/6638080/is-there-os-written-in-haskell

                        … Можно продолжать еще. Но зачем? Я уже в который раз наблюдаю, как люди с той стороны баррикад говорят, что если на языке X никто не пишет и не вздумает писать Y, значит это язык плох и ужасен. Очень часто под Y упоминают ваш список: 3D-игры, ядро/модули ОС, фоторедакторы… Фантазия, правда, этим и ограничивается. По-моему, пора бы уже понимать, что категорий софта десятки, если не сотни, и для большинства из них нет объективных причин использовать только мейнстримные языки. Например, я бы сейчас крепко задумался, если бы мне предложили писать MMO-игру на С++, а не на Erlang, просто потому что в С++ нет (нативно) этой замечательной концепции — модели акторов. Я боюсь себе представить, насколько сложный код получился в компании CCP…

                        И модель погоды я бы взялся писать не на С++, а на Haskell или Scala. Просто потому что мне бы хотелось иметь чистые функции, распараллеливаемые алгоритмы, кристально ясный код.
                        • +1
                          Уже потом я нашел тесты по производительности. Scala отстает всего на 25% от C++ (правда, памяти жрет в разы больше). Это очень хороший результат, я ожидал 5-10-кратного падения производительности. А при 25% можно писать почти все, что угодно, согласен.
                        • 0
                          Ах, да еще: High Freequency Trading на Haskell — это реально. Меня приглашали пособеседоваться в какую-то западную компанию, которая пишет HFT систему на этом языке.
                          • 0
                            Не уверен, что они пишут на Haskell нижний сетевой уровень, а верхний аналитический — почему нет? :)
                            • 0
                              Детали мне, к сожалению, не известны. По сочетанию HFT + Haskell гуглится много материала, но я не вчитывался, лениво.

                              Еще хотел бы упомянуть, что на Erlang весьма комфортно описывать протоколы нижних уровней, так как там есть удобная манипуляция данными на уровне битов (если я не ошибаюсь).
                          • 0
                            Однако, никто не будет писать модель погоды на… Scala, как и не будет на них писать систему торгов на бирже… базу данных, сервер сообщений и т.д. и т.п.

                            Scala с такими задачами отлично справляется. Например, ElasticSearch, OrientDB, Hadoop, Apache Spark тому доказательство. Да, они написаны на Java, но всё что написано на Java может быть написано и на Scala.
                              • 0
                                Уже согласился с большинством оппонентов, найдя и посмотрев тесты по сравнительной производительности (которых я и просил с самого начала, но которые никто так и не дал).
                                Однако…
                                Да, они написаны на Java, но всё что написано на Java может быть написано и на Scala.

                                — совершенно нелогичное утверждение, даже если вторая его часть и верна.
                            • 0
                              Ну вот хотя бы что-то:
                              togototo.wordpress.com/2013/08/23/benchmarks-round-two-parallel-go-rust-d-scala-and-nimrod/
                              Кстати, неплохая производительность.

                              Гугл тоже делал тесты в 2011, но что-то я не доваеряю их результатам (как разработчика Go):
                              readwrite.com/2011/06/06/cpp-go-java-scala-performance-benchmark
                        • +1
                          В большинстве программных продуктов большинство мест не являются узкими местами ни по скорости выполнения, ни по потребляемой памяти. Если же такие места будут выявлены, то их можно переписать на Java/C++/ASM. В современном мире гораздо важнее поддерживаемость и удобство (а, значит, и скорость с безошибочностью) разработки.
                          • 0
                            В большинстве программных продуктов большинство мест не являются узкими местами ни по скорости выполнения, ни по потребляемой памяти

                            Придерживаюсь обратного мнения, при опыте в 29 лет профессионального программирования.

                            В современном мире гораздо важнее поддерживаемость и удобство (а, значит, и скорость с безошибочностью) разработки.

                            Опять же, мой обширный реальный опыт говорит об обратном.
                            • +2
                              Нисколько не умаляя Вашего опыта хочу заметить, что 29 лет назад реалии разработки ПО были иными, и требования к программам были иными, сложность ПО тоже многократно выросла.
                              Вы говорите о том, что программа приемлема, если она работает не дольше определённого времени и потребляет не больше определённых ресурсов — по этим критериям Scala приемлема. Но ценность Scala в первую очередь не в этом, а в том, что она даёт программисту больше различных средств кода, повышая модульность и поддерживаемость (и, косвенно, отказоустойчивость). Я думаю, что не ошибусь, если скажу, что предыдущие комментаторы имели ввиду то, что эти свойства в современном мире важнее, чем экономия каждого байта, с чем я полностью согласен.
                              • 0
                                29 лет назад реалии разработки ПО были иными, и требования к программам были иными, сложность ПО тоже многократно выросла.

                                Да что ж вы все так упорно приписываете мне слова, которых я не говорил? Я не говорил, что 29 лет назад я работал программистом. Я начал им работать 29 лет назад, но работаю и сейчас, и прекрасно знаю современное состояние отрасли. Более того, первый проект, в который меня ввели 29 лет назад, имел полтора миллиона строк кода. Так что насчет сложности…
                                И опять, я нигде не сказал про экономию каждого байта. Зачем передергивать?
                                • +2
                                  Сейчас уже в индустрии есть понимание, что нельзя хвастаться огромным количеством строк кода. Зато можно хвастаться малым количеством строк, которые делают то же самое.

                                  Что-то вот такое я встречал в сети (интерпретация авторская, как в оригинале — не помню):

                                  «Пришел к нему один ученик.
                                  — Учитель! Я хочу достичь просветления!
                                  — А сколько у тебя строк кода в том простом проекте, что я задавал?
                                  — 100 000 строк, много комментариев, везде защитное программирование.
                                  — Молод ты еще. Убери бессмысленные assert и комментарии — пусть код сам за себя говорит, тогда и возвращайся.
                                  Вернулся позже к нему ученик:
                                  — Учитель! Я убрал все лишние комментарии, заменил все assert и проверки на NULL на чистые функции, добавил юнит-тесты. У меня получилось 50 000 строк. Теперь-то я могу достичь просветления?
                                  — Нет, ученик мой, все еще нет. Узнай про KISS и DRY, изучи библиотеки, отрефактори код и тогда уже возвращайся.
                                  Вернулся позже к нему ученик:
                                  — Учитель! Я отрефакторил код. Я убрал повторы, обобщил код, заменил велосипеды билиотечными аналогами. У меня получилось 10 000 строк! Теперь-то я могу достичь просветления?
                                  — Да, теперь можешь. Вот посмотри, это мой код, который делает то же самое, что и твой. Здесь 500 строк.
                                  И тогда ученик достиг просветления.»
                                  • +2
                                    Опять мимо. Я: 1) прекрасно знаю «понимание в индустрии» и 2) не хвастался, но лишь аргументированно оспорил утверждение, что 29 лет назад системы были «многократно проще».

                                    Но самый главный аргумент против моего же мнения я нашел сам — сравнительные тесты производительности по разным языкам (см. выше). Scala там очень неплохо смотрится.
                          • 0
                            Ну и напоследок, «конкурентные программы» — это ж надо так перевести! В-)

                            Я считаю, что люди переводящие книги будут опытнее меня в переводах, вот отрывок из книги «Семь моделей конкуренции и параллелизма за семь недель»:
                            dmkpress.com/files/PDF/978-5-97060-244-7.pdf

                            Пример названия главы: «Конкурентные программы для конкурентного мира».

                            Что именно вас смутило в данном словосочетании?
                            • 0
                              В русском языке выражение «конкурентные программы» означает программы, конкурирующие между собой — не внутри себя, а между собой. То есть, когда программы конкурируют с другими программами.
                              И то, что какой-то переводчик перевел этот термин так неудачно, не значит, что надо его использовать.
                              • 0
                                Подскажите, пожалуйста, другой термин, чтобы я им пользовался.
                                • 0
                                  slovari.yandex.ru/concurrent/%D0%BF%D0%B5%D1%80%D0%B5%D0%B2%D0%BE%D0%B4/

                                  Вообще конкурентный это competitive — а concurrent это параллельно выпольняющийся
                                  • 0
                                    Да хотя бы многопоточные.
                                    • 0
                                      Благодаря выступлению Роба Пайка, всегда, когда мне попадается слово «concurrency» в тексте, то сразу приходит на ум фраза «Concurrency is not parallelism». Термин же «многопоточные» на английском — multithreading. Получается, имеем два термина на английском: «concurrency» и «multithreading», но на русском они должны сливаться в один? Я считаю это неправильным.

                                      В википедии фразу «Concurrency and data structures» переводят как «Одновременность и структуры данных». Всё же для меня слово «concurrency» по смыслу ближе к одновремменому или конкурентному, чем к многопоточному.
                                      • 0
                                        Лучше «concurrency» и «multithreading» будут сливаться в один термин (потому что технически они одно и то же и обозначают, только с разными акцентами), чем использовать по-русски неправильное выражение.
                                        • +1
                                          Подведу итог.

                                          1) Программы, конкурирующие с другими программами, называются «конкурирующими программами», а не конкурентными (конкурирующий vs конкурентный).
                                          2) Во многих источниках, связанных с техническим термином «concurrency», используется перевод «конкурентный» или «одновременный», а не «многопоточный».
                                          3) Термин «многопоточный» на английском языке пишется как «multithreading», а не «concurrency».

                                          Для себя делаю вывод, что выражение «конкурентные программы» правильное и ничего в нём «это ж надо так перевести» нет. Вы, со своей стороны, можете спокойно считать его неправильным. Засим считаю тему закрытой.
                          • –6
                            Что-то кроме наличия/отсутствия неизменного состояния я не увидел никаких гипотетических преимуществ Scala перед ruby. Опуская неясность с вопросом «чем же хорошо неизменное состояние» (идиот справится напакостить и вообще между одних констант), хотелось бы спросить автора (за неимением гербовой, может быть вы, как переводчик, растолкуете :), зачем тут вообще руби? Ну императивная Java автору надоела, это понятно. Scala для его задач круче — тоже ясно. Руби-то тут зачем?

                            Ну и, чтобы два раза не вставать: `strToInt` это же гораздо хуже отсутствия неизменного состояния. Что у нее с областью видимости? Один раз определил и теперь все имена пользователей, состоящие только из цифр, автоматом стали numeric? Не говоря о том, что `[1, '10'].map(&:to_i).min` в десять раз короче и в миллион раз понятнее.
                            • +5
                              Основное преимущество — статическая типизация, не нужно писать тесты на такие мелочи, как опечатки, и в целом увереннее себя чувствуешь, если скомпилировалось, значит, скорее всего, будет работать. Оно и в Java так, но в Java постоянно нужно обходить несовершенства системы типов, а здесь с этим попроще.
                              А неизменяемое состояние хорошо своей простотой — меньше сущностей при чтении кода, проще работать.
                              • +2
                                Статическая типизация сама по себе врядли кого-то волнует.

                                не нужно писать тесты на такие мелочи, как опечатки,

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

                                • +3
                                  Грамотное применение типов может уменьшить число тестов в разы. Зачем мне делать assert(x.length=y.length), если у меня по системе типов в функцию передается список пар [(x,y)]?
                                  Лично я за языки с опциональной типизацией типа Groovy, Julia или в какой-то степени Clay и шаблоны C++, а не за Haskell и Scala, потому что это позволяет сначала сделать динамический прототип, а потом отрефакторить, расставив типы, но это вопрос такой. Совсем без типов тяжко.
                                  • 0
                                    >а потом отрефакторить, расставив типы

                                    «Нет ничего более постоянного чем временное» (с) кто-то умный
                                    • 0
                                      Это не так долго, если выставить типы у корневой функции, то они выведутся сверху вниз до самой нижней. Если возникнет пара неоднозначных мест, можно пофиксить. Но изначальный код пишется в динамике, и мне это нравится больше, чем хаскельный подход с «сначала полчаса думаем, какие у нас будут типы».
                                      • 0
                                        так зачем прототипировать без типов, когда можно сразу с типами?
                                        • 0
                                          Тогда приходится тратить время на редактирование «спецификации» типов, которая вполне может пожить и в голове. Редко когда с самого начала угадаешь с типами. Конечно, если не угадал, то и код нужно переписать, но только по месту использования, а не в двух местах.
                                          А так 'Нет ничего более постоянного чем временное' тоже работает, если проще 100% покрыть тестами, проще покрыть тестами. В общем, такие языки позволяют использовать динамику, там где это удобно, и статику где удобно. Пишется полно всякого обслуживающего кода — отчеты там всякие, и тут можно один язык использовать. В случае С++ приходится тягать функции из питона через SWIG или заниматься прочей ерундой. На JVM полегче, но все равно один язык довольно больше удобство.
                                          • 0
                                            >>Тогда приходится тратить время на редактирование «спецификации» типов, которая вполне может пожить и в голове.

                                            Каждому своё. По мне так языки с выведением типов в этом отношении не отличаются от динамических: если «не угадал с типами» переписывать приходится тоже только в одном месте
                                            • 0
                                              Из собственного опыта могу сказать, что в хаскелле ситема типов, обычно, не мешает. Как раз наоборот. И заставляет разрабатывать программу сверху-вниз. Т.е. сначала вы определяетесь чего вы хотите, какой тип у того, что вы хотите. Потом, вниз по цепочке вы определяете какие промежуточные типы вам нужны и так вплоть до тех типов что у вас есть на входе. Затем, пишите функции которые бы преобразовывали один типы в другие(соединяете стрелками, ага) и вуаля. Очень удобно прикидывать на бумажке или в каком-нибудь редакторе диаграмм (я люблю dia).
                                              • 0
                                                В том-то и дело, что система типов в хаскеле требует определенный стиль разработки — сначала семь раз отмерить, какие тебе типы нужны (ADT или кортежей достаточно? а тут у нас список или словарь нужен? или два словаря?), а потом отрезать код. А я предпочитаю скульптуры не из каменя высекать, а из пластилина лепить. Вылепил, а дальше прибил гвоздями типы с помощью аннотаций. В этом плане мне больше всего нравится язык Clay, в котором я даже поучаствовал немного, ныне застывший.
                                                • 0
                                                  > из пластилина лепить
                                                  > язык Clay

                                                  Из глины, быть может? А чтобы Clay не застыл, его надо было водичкой смачивать.

                                                  По существу: реализованные типы — это половина решения задачи. На самом деле, больше думаешь, а не является ли предметная область чем-то большим, может это zipper или монада, или комонада? И если да, код упрощается в разы.
                                      • 0
                                        Если мы передаем два списка произвольной длинны, то в любом случае придется проверить их длинну.
                                        А если список ровно из N элементов, то может быть в этом случае при статической типизации уберется ассерт, но добавятся аннотации.
                                        Не вижу тут явного преимущества.

                                        • 0
                                          Грамотное применение типов может уменьшить число тестов в разы. Зачем мне делать assert(x.length=y.length), если у меня по системе типов в функцию передается список пар [(x,y)]?


                                          Еще раз перечитал. Теперь понял о чем ты.
                                          Но тут вовсе нет разницы, кто тебе мешает в языке с динамической типизацией передать список пар.
                                          Так например работает конструктор dict в питоне — получает список пар (и не обязательно список а любой итерируемый объект). И что неужели тут случются какие-то проблемы?
                                          • +2
                                            Могу более жизненный пример дать, от Джоэля. Два типа unsafe_string, и safe_string, первый всегда получается от юзера, и может потенциально содержать XSS, инъекцию или что-то подобное, а safe_string — безопасная строка. safe_string неявно конвертится в unsafe_string, а unsafe_string в safe_string нет, только методом escape. Все функции, работающие с пользовательским вводом, возвращают unsafe_string. Все функции, кладующие что-то в базу или выводящие принимают safe_string. В итоге компилятор сам следит за тем, чтобы не было вредного кода в пользовательском коде, программист забыть не может.

                                            И в принципе, как и в случае со списками, подобное можно написать и на динамическом языке, но есть один момент. Точнее два. Первый момент заключается в том, что человек без опыта статических языков вряд ли вообще о таком додумается — что строки могут иметь разные типы для помощи в проверках. Но это ерунда. _Принципиальная_ разница в том, когда упадет, если что-то написано не так. В статическом языке упадет при компиляции, в динамическом — во время теста (если повезет), или во время показа заказчику (если не повезет и ты как все забиваешь на тесты). И чем мощнее система типов, тем больший процент ошибок можно перенести на время компиляции. В Java, например, из-за ее фатальных недостатков, многие вещи проще делать через даункасты из Object — это та же динамическая типизация по сути. Все эти NullPointerException, ClassCastException — это вовсе не обязательно, чтобы в рантайме падало.

                                            Вот чтиво про тесты в статических языках:
                                            spin.atomicobject.com/2014/12/09/typed-language-tdd-part1/
                                            spin.atomicobject.com/2014/12/10/typed-language-tdd-part2/
                                            • –2
                                              Про unsafe_string тут опять не понятно, при чем тут типизация.

                                              Вот смотри если ты не используешь TDD то программа у тебя упадет в любом случае, и тут никакая статическая типизация не поможет. А если используешь то очепятка или забытый импорт отловится на этапе запуска тестов.
                                              Кроме того, чтобы допустить очепятку нужно неплохо постараться, быть упертым и уверенно идти к этой цели — игнорировать автодополнение и игнорировать варинги среды разработки.
                                              Да я много слышал подобных рассуждений, от людей которые проповедуют статическую типизацию. На деле таких ошибок почти никогда не происходит. Зато я постаянно вижу стэктрэйсы от всяких API на Яве, в том числе в досаточно серьзеных проектах. Почему-то не спасает их статическая типизация.

                                              • +2
                                                >Про unsafe_string тут опять не понятно, при чем тут типизация
                                                При том, что в статике тебе не нужно тестов, чтобы проверять эти случаи. Вообще. Если забыл где-то — оно не скомпилируется. И так как это бесплатно (что в плане производительности, что в плане затрат времени), этим можно пользоваться. Потратил время, а потом забыл.

                                                >Вот смотри если ты не используешь TDD то программа у тебя упадет в любом случае, и тут никакая статическая типизация не поможет
                                                Да нет, поможет. Хардкорное TDD вообще редко используют в статических языках, потому что не нужно. Тесты пишут уже после кода. Дилемма простая, в статике ты описываешь типы и пишешь мало тестов, а в динамике ты пишешь много тестов, очень много тестов, нужно больше тестов. И в чем профит, в экономии времени? Так в динамике время реально экономится, когда тестов не пишешь вообще :) А если их писать, то времени приходится тратить больше, чем на типы… Это хорошо видно по распределению проектов — динамика в основном используется как CRUD примочка к базе, которой упасть в принципе не страшно, для скриптов автоматизации, для прототипов и расчетов… А статика — когда куча умных перцев готовит проект на годы. И первоначальные затраты времени окупаются сторицей. Я ничего не проповедую, говорю как есть просто. У меня специфика такая, что я за динамическими языками провожу больше времени, мне холиварить вообще не о чем.
                                                >На деле таких ошибок почти никогда не происходит.
                                                Ну это легко проверить, посмотрев на багтрек любого проекта.
                                                >Зато я постаянно вижу стэктрэйсы от всяких API на Яве, в том числе в досаточно серьзеных проектах.
                                                Я написал выше, ява очень часто вынуждает динамически кастить типы, этот язык не совсем показатель.
                                                >Почему-то не спасает их статическая типизация.
                                                Но это и не панацея, но вопрос же не стоит «зачем мучиться со статикой, когда есть такая хорошая динамика». Патчить зрелый статический проект, в котором все типы давно написаны, намного приятнее, чем ковыряться в динамическом проекте в 10 раз меньше. Писать с нуля проще и быстрее скриптами…
                                                • 0
                                                  Да нет, поможет. Хардкорное TDD вообще редко используют в статических языках, потому что не нужно. Тесты пишут уже после кода. Дилемма простая, в статике ты описываешь типы и пишешь мало тестов, а в динамике ты пишешь много тестов, очень много тестов, нужно больше тестов. И в чем профит, в экономии времени? Так в динамике время реально экономится, когда тестов не пишешь вообще :) А если их писать, то времени приходится тратить больше, чем на типы… Это хорошо видно по распределению проектов — динамика в основном используется как CRUD примочка к базе, которой упасть в принципе не страшно, для скриптов автоматизации, для прототипов и расчетов… А статика — когда куча умных перцев готовит проект на годы. И первоначальные затраты времени окупаются сторицей. Я ничего не проповедую, говорю как есть просто. У меня специфика такая, что я за динамическими языками провожу больше времени, мне холиварить вообще не о чем.


                                                  Кажется мы тут уже ушли в другую тему «Писать тесты или не писать».
                                                  TDD (здесь я имею ввиду не именно TDD, а любые методики написания тестов) не используют в двух случаях, первый случай — это когда не умеют писать тесты, а второй случай когда затраты на тесты не окупятся, т.к. это какой-то одноразовый не критичный код.
                                                  У меня в проектах полно тестов, но там нет каких-то специфических тестов связанных с типизацией.
                                                  Вот выше в статье например пример с вычислением чисел фибоначи. Чтобы определить, что эта функция работает, статическая типизация никак не поможет. Мы напишем функцию которая будет всегда возвращять ноль и она спокойно скомпилируется Чтобы убедится что функция работает, нужны юнит тесты. А они нам заодно и будут гарантией от очепяток и забытых импортов, в случае динамического языка. Но тут нет никакой разницы можду статическим и динамическим, набор тестов будет один и тот же.
                                                  В чем экономия времени. Очень просто.
                                                  Вот мы написали тест и функцию. А потом нам понадобилось изменить реализацию, например сделать рефакторинг или оптимизировать по скорости или выпились из кода устаревушю библиотеку. Если тестов нет, то придется заного проходить весь цикл тестирования, а если тесты есть, то этого не требуется.


                                                • 0
                                                  Можно подумать, от TDD она перестанет падать.
                                                  Зато я постаянно вижу стэктрэйсы от всяких API на Яве, в том числе в досаточно серьзеных проектах. Почему-то не спасает их статическая типизация.

                                                  Так выше уже написали, что статика статике рознь, и много где можно наткнуться на динамические рудименты.
                                      • 0
                                        Попытку вызвать метод, не определенный на данном типе, не заметить мгновенно в руби очень трудно. Даже если специально писать код вслепую. «если скомпилировалось, значит, скорее всего, будет работать» — ой, я абсолютно не согласен. Будет запускаться — да. Алгоритмические ошибки обычно связаны не с тем, что с типом напутали (по моему опыту).

                                        Ну а «неизменяемое состояние хорошо своей простотой» — это какая-то мантра, а не аргумент. Что в неизменяемом состоянии простого? Особенно в chain, как в авторских примерах?

                                        • +3
                                          Всмысле «не заметить очень трудно»? Упадет в рантайме и привет. А опечатки возникают при мерже в git, например — это не всегда перед глазами. Приходится писать тесты на то, на что в статическом языке тестов не стал бы писать, потому что и так все работает.

                                          В неизменяемом состоянии просто то, что если конструктор правильный, то состояние сущности всегда правильное. То есть конструктор и методы можно оттестировать отдельно, а в случае изменяемого состояния результат теста может зависеть от порядка вызова методов — что, естественно, никто в жизни тестами не покроет. Читать код удобно тоже — функции превращаются в трубы, которые преобразуют сущности.
                                          • –9
                                            Вот в этом именно смысле. Какая разница, упадет в рантайме, или при компиляции? Еще раз повторяю: если падает (или отлавливается компилятором) — это не ошибка, это описка. Которую одинаково легко поймать вне зависимости от языка (ну, кроме ассемблера и plain c).

                                            Ох, мама дорогая. А с изменяемым состоянием трубу не написать, что ли? Если сеттеров, явных и неявных, у объекта нет, то будь он триста раз изменяемый, ничего от порядка вызова не зависит. В оригинальном тексте автор боится, что между map и reduce какая-то злая инопланетная тварь ему коллекцию испоганит. Это надуманная ерунда.

                                            Короче, мы тут выучили умные слова, смысл не разумеем, но повторяем и радуемся. Ни одного _аргумента_ я так и не услышал. Вон мне там даже внизу написали, что «смысл в том, чтобы показать преимущества функционального подхода перед императивным». Ну так возьмите эрланг, у него-то преимуществ полно́ (и есть продакш.версия, в отличие от, например, хаскеля). Нет, возьмем скалу и высосем из пальца кучу преимуществ.

                                            На простом php можно запросто писать используя только функциональный подход. И на джаве можно (придется поиграть в шахматы с коллекциями и рефлекшеном). И на шелле можно. Hadoop en.wikipedia.org/wiki/Apache_Hadoop, например, классическая имплементация мап-редьюса на (опа) джаве.
                                            • 0
                                              и есть продакш.версия, в отличие от, например, хаскеля

                                              На сколько хорошо вы разбираетесь в данной теме?
                                              • +1
                                                Я дискуссии в стиле ad hominem не веду, уж извините.

                                                P.S. «Насколько» тут пишется слитно, если это не вопрос про мою зарплату, конечно.

                                                • +2
                                                  Мне не интересна ваша зарплата, мне было интересно на чём вы основываете данное утверждение, на собственном опыте или вам бабка во дворе сказала.
                                              • +17
                                                >>Какая разница, упадет в рантайме, или при компиляции?

                                                O_o вы это серьёзно?
                                                • +3
                                                  >Какая разница, упадет в рантайме, или при компиляции?
                                                  Огромная. При компиляции оно упадет при мне, а когда падает в рантайме — ну, если повезет, выловит тест, но раз в год и палка стреляет, а реальные люди почему-то отличаются от отличников из книжек по TDD, особенно когда сдача проекта вчера.

                                                  >А с изменяемым состоянием трубу не написать, что ли?
                                                  Я пишу «Читать код удобно», а вы мне что? Можно зделоть? Не понятно, о чем разговор, любовь к неизменяемости вообще с функциональностью слабо связана. const везде писать и в C++ рекомендуют, и у Макконнелла что-то подобное есть.

                                                  >В оригинальном тексте автор боится, что между map и reduce какая-то злая инопланетная тварь ему коллекцию испоганит. Это надуманная ерунда.
                                                  В оригинальном тексте много надуманной ерунды, да. Но это больше из-за вводного формата статьи. Сейчас из функциональщины взято все, что может быть легко перенесено в императивные языки, и поэтому особо такие статьи не впечатляют. Ну лямбды, ну вывод типов, а у нас вон linq и var есть. А на Rust если посмотреть, там и тайпклассы, и паттерн матчинг. Теперь впечатлять могут только вещи, которые уже так просто в императивный язык не перенесешь, потому что там все завязано на особенности этих языков. Типа vector fusion в хаскеле. А Scala лучше Java прежде всего как императивный язык, многие ее хотя бы из-за этого выбирают.

                                                  >На простом php можно запросто писать используя только функциональный подход.
                                                  Совсем унылые аргументы языкосрача пошли. Ну делойте, раз можно зделоть, вас кто-то насильно скалкой кормит что ли.
                                                  • 0
                                                    Я с самого начала говорил то же самое, что сейчас говорите вы. Я же статьей неудовлетворен, а не скалой.

                                                    Забавно, вы в предпоследней реплике, например, пишете чуть не дословно то, что в последней реплике обзываете «языкосрачем».

                                                    А, ну и про рантайм. Не знаю, как вы, а я ни разу не сталкивался с проблемой «все сломалось из-за ошибочного типа». Обычно все ломается из-за того, на что тест толком не написать. И императивный тут язык, функциональный ли, разница невелика.
                                                    • +1
                                                      >Забавно, вы в предпоследней реплике, например, пишете чуть не дословно то, что в последней реплике обзываете «языкосрачем».
                                                      Я не против языкосрача, я против унылых аргументов. Когда идет аргумент, что на php можно сделать, ну да — вот github.com/ircmaxell/monad-php, но кто в здравом уме будет это использовать.

                                                      use MonadPHP\Identity;
                                                      $monad = new Identity(1);
                                                      $monad->bind(function($value) {
                                                      return 2 * $value;
                                                      })->bind(function($value) {
                                                      var_dump($value);
                                                      });

                                                      Функциональное программирование, это же не просто комбинаторы комбинировать а-ля linq to objects, как во времена SICP, в функциональной среде постоянно рождается что-то новое, что переходит в мейнстрим, сейчас это куча дополнительных фич и принципиально другой способ кодирования, который на императивных языках выльется в кашу из лямбд и тернарных if'ов вместо паттернматчинга.

                                                      >Не знаю, как вы, а я ни разу не сталкивался с проблемой «все сломалось из-за ошибочного типа».
                                                      А это не всегда видно, что типы могут помочь. Например, от всего пласта XSS ошибок и sql-инъекций в статическом коде можно избежать парой классов, но если рассуждать, что все строки одинаково полезны — уже не получится. От пласта ошибок единиц измерения, от которых ракеты падают, между прочим, тоже можно типами.
                                                      Но здесь дилемма статика/динамика непринципиальна (любые статические типы можно проверять и в динамике, и падать в рантайме), а вот сам факт падения при компиляции штука очень нужная и полезная.
                                          • 0
                                            Один раз определил и теперь все имена пользователей, состоящие только из цифр, автоматом стали numeric?

                                            На самом деле нет.
                                            Будет производиться дополнительная проверка, число мы получили или нет, и если нет, существует ли способ привести полученное к числу.
                                            • 0
                                              Суть данной статьи не в том, чтобы показать преимущества Scala перед другим языком программирования, а в том, чтобы показать преимущества функционального подхода перед императивным. В примерах Scala можно с лёгкостью заменить на Haskell и получить тот же (возможно даже и лучший) результат. И если вас действительно заинтересовало функциональное программирование, то как-раз советую присмотреться к Haskell (а потом вернуться к Scala и понять насколько этот ЯП крут :).
                                              • 0
                                                В дополнение. В последней книге Одерски о Scala есть замечательный пример функционального подхода, где авторы в одной из глав рисуют спираль. Просто коротко и передает изящность функционального подхода.
                                            • +1
                                              Будучи программистом на Ruby, строгая типизация в Scala ощущалась в начале как бремя…

                                              Стилистическая ошибка.
                                              • –1
                                                Подъезжая к сией станции и глядя на природу в окно, у меня слетела шляпа.
                                                • 0
                                                  Поправил, надеюсь стало лучше.
                                                • 0
                                                  В отличие от примера на Java, код приведён полностью, т.е. можно его запускать в таком виде и он будет работать.

                                                  Неверно. В Scala, так же как и в Java, точкой входа является статический метод main (скорее всего это ограничение jvm). Мы либо явно его определяем, либо наследуем трейт App. Если речь идёт о repl, то там код выполняется в теле неявно созданного App. Так что от main никуда не деться.
                                                  • +1
                                                    Да, имелось в виду, что этот код можно запустить в REPL. Просто тем, кто не знаком со Scala может показаться что в этом коде так же пропущены геттеры, сеттеры и конструктор, как в примере с Java.

                                                    И если совсем уж заморочиться, то можно обойтись и без main с App:

                                                    #!/usr/bin/env bash
                                                    exec scala "$0" "$@"
                                                    !#
                                                    
                                                    println("Hello!")
                                                    
                                                  • 0
                                                    В Scala for-генератор – это синтаксический сахар для функциональной композиции.

                                                    Э… Что, простите?

                                                    for — синтаксический сахар для filter/flatMap/map, но никак не функциональной композиции.
                                                    • 0
                                                      Основывал своё высказывание по доке:
                                                      http://docs.scala-lang.org/tutorials/FAQ/yield.html

                                                      «Scala’s “for comprehensions” are equivalent to Haskell’s “do” notation, and it is nothing more than a syntactic sugar for composition of multiple monadic operations.»

                                                      • +1
                                                        «composition of multiple monadic operations» — композиции нескольких монадических операций. Это никак не функциональная композиция, да и не композиция вовсе на самом деле — просто-напросто, слегка завуалированный fluent interface
                                                        • +1
                                                          Вполне себе композиция функций. x.f().g() === g'(f'(x)) === (g'. f') x
                                                          • 0
                                                            В вашем тождестве эквивалентен результат, но не способы его получения.
                                                            И написать val mapFilter = map(_+1) . filter(_ > 0) в Scala не получится — map/filter, это не полиморфные функции, а методы класса.
                                                            Никакой композиции нет во fluent-интерфейсе, а есть лишь последовательность вызовов.
                                                            • 0
                                                              Вы правы, с композицией функций я погорячился.
                                                            • 0
                                                              g(f(x)) это не композиция функций а вызов. Композиция была бы

                                                              g. f где результатом была бы функция
                                                        • –4
                                                          Скорее всего это filter/flatMap/map синтаксический сахар для for.
                                                        • 0
                                                          Cтоит отметить, что при объявлении функции не обязательно было указывать тип возвращаемого значения, компилятор Scala выведет его сам.

                                                          Для рекурсивных функций обязательно.
                                                          • 0
                                                            Код Scala компилируется в байт-код для JVM. Можно ли взять программу на Scala и переписать ее на Java так, чтобы на выходе был практически идентичный байт-код?
                                                            • 0
                                                              Можно, но зачем? )
                                                              • 0
                                                                Интересно, как устроены хитрые функциональные штуки и как можно их реализовать в чистой Java.
                                                                • 0
                                                                  В принципе, если посмотреть исходники, там нет ничего невозможно сложного или радикально необычного. Многие Scala-specific вещи (например, с типами) не спускаются на уровень JVM, другие имитируются с помощью стандартный JVM-механизмов (например, lazy-значения сделаны через double-checked locking с битовым полем, а объекты-компаньоны — через статические классы с постификсом .MODULE$)
                                                              • 0
                                                                Иногда можно, но scala-library окажется в зависимостях (а довольно много магии в ней). Некоторые вещи из скалы нельзя будет напрямую реализовать на яве.
                                                              • 0
                                                                В scala есть возможность получить байткод, невозможный на java. Самый простой пример: оптимизация хвостовой рекурсии с заменой this.
                                                              • 0
                                                                Примеры на Java несколько натянутые. Даже если забыть про Java 8, есть такая де-факто стандартная библиотека, как guava collections, которая в себе имеет и функциональные операторы и аналог Option и отличие от скалы будет только в несущественном синтаксическом мусоре. Есть много других примеров того, как гибкость Scala позволяет делать очень лаконичный код. Например монадические комбинаторы парсеров из стандартной библиотеки.

                                                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.