среда, 26 января 2011 г.

За что мне не нравится Java. Проверки на null


Хотя сам занимаюсь разработкой на Java, язык этот мне кажется чересчур громоздким и многословным. Много в нем недостатков, но сегодня коснусь одного, уж очень сильно раздражающего.
При вызове методов постоянно нужно следить за null-ами. Написал метод – будь добр, проверь все параметры на null. Вызвал метод – проверь полученное значение на null. Делать это нужно постоянно, а исходник страдает обилием проверок, здорово захламляющих логику.
В отличие от Java, язык Clojure, наследник Lisp-а, позволяет изменять собственный синтаксис. Главная фишка Clojure – макросы и программирование на уровне абстрактного синтаксического дерева. Воспользуемся этими вкусностями и автоматизируем проверку на null всех параметров функции.

(defn nil-parameter [f] (println "One of the parameters of the function" f "is nil!"))

(defmacro defn-checked [name parameters & body]
   `(defn ~name ~parameters
      (if (or ~@(map (fn [p] `(nil? ~p)) parameters) )
        (nil-parameter (str ~name))
        (do ~@body ))))

Теперь можно создать функцию при помощи новой синтаксической конструкции defn-checked также, как и при помощи defn.

(defn-checked my [p1 p2] (println "123"))

На стадии компиляции исходника Clojure преобразует этот код в следующий.

(defn my [p1 p2]
   (if (or (nil? p1) (nil? p2))
     (nil-parameter (str my))
       (do
         (println "123"))))

Чуть сложнее делать проверки возвращаемых значений. Показанный ниже макрос использует возможности программирования на АСТ для преобразования списка bindings.

(defn nil-value [] (println "One of the values is nil!"))

(defmacro let-checked [bindings & body]
   (let [indexed-bindings (map vector (iterate inc 0) bindings)
          symbols (map #(second %) (filter #(even? (first %)) indexed-bindings))
          values (map #(second %) (filter #(odd? (first %)) indexed-bindings))
          results (gensym "results")
          indexed-symbols (map vector symbols (iterate inc 0))]
          `(let [~results ~(vec values)]
              (if (not (empty? (filter #(nil? %) ~results)))
                (nil-value)
                (let [~@(reduce join 
                                          (map (fn [s] [(first s) 
                                                   `(nth ~results ~(second s))]) indexed-symbols))]
                ~@body)))))

Теперь можем использовать новую конструкцию let-checked также, как и let:

(let-checked [a 5
                    b (+ 1 2)]
  (+ a b))

На стадии раскрытия макросов Clojure создаст следующий код.

(let [results [5 (+ 1 2)]]
   (if (not (empty? (filter (fn [p] (nil? p)) results)))
     (nil-value)
     (let [a (nth results 0)
            b (nth results 1)]
       (+ a b))))

Как видите, вначале будут вычислены все значения и занесены в вектор results. Затем произойдет проверка, есть ли хоть один null среди значений results. И только если все значения не равны null, произойдет привязка новых символов a и b к элементам вектора results, и будет выполнено тело выражения.
А ваш язык программирования может такое?

18 комментариев:

  1. Офигеть как удобно. Вместо того чтобы использовать систему типов с явной обнуляемостью значений будем городить (пусть и с помощью макросов) рантайм-проверки.

    ОтветитьУдалить
  2. Вообще, наверное, это дело вкуса. Здесь я просто хотел показать, что clojure -- более гибкий инструмент, позволяющий автоматизировать то, что постоянно приходится писать вручную на java. Как ни крути, но на java будут все те же самые рантайм-проверки, только писать их будет не макрос, а программист.

    ОтветитьУдалить
  3. А как быть с проверкой на null вложенных объектов?

    class Person {
    public Address Address { get; set; }
    }

    class Address {
    public string PostCode { get; set; }
    }

    class Program {
    static void Main()
    {
    if (person == null ||
    person.Address == null ||
    person.Address.PostCode == null)
    {
    throw new NullReferenceException();
    }

    Console.Write(person.Address.PostCode);
    }
    }

    ОтветитьУдалить
  4. Такую многоуровневую проверку тоже несложно реализовать при помощи макросов, как-нибудь покажу на досуге. И тебе здесь не удастся меня подловить: я могу изменить синтаксис Clojure, а ты в своем C# -- нет :-)

    ОтветитьУдалить
  5. В объектно-ориентированных языках уже достаточно давно придумана такая вещь, как Null Object Pattern. Последовательно используя этот приём, можно избавиться от большинства проверок на null без риска словить исключение.

    Конечно, этот шаблон подходит не во всех случаях (например, если мы пишем библиотеку, которую может использовать кто угодно, придётся подстраховаться). Но для большинства практических случаев он является удобным и элегантным решением.

    ОтветитьУдалить
  6. Да, действительно, если мы сами делаем API, то мы можем разработать методы, которые никогда не вернут null. А что если я использую уже готовые сторонние решения, для которых null -- вполне нормальное возвращаемое значение?
    Кроме того, реализовывать NullObject для каждого класса -- это еще больше лишнего кода, чем проверки на null.

    ОтветитьУдалить
  7. Да ну. Null Patern для каждого класса - роскошь и не оправданная. К тому же, зачастую (да в большинстве реальных задач!) действительно нужно вернуть null.
    Другое дело - перейти к монадическому синтаксису или использовать возможности библиотеки PostSharp.

    ОтветитьУдалить
  8. Посмотрел PostSharp. Честно говоря, не представляю, как с ее помощью реализовать хоть что-то похожее на синтаксическую конструкцию let-checked.

    ОтветитьУдалить
  9. Этот комментарий был удален автором.

    ОтветитьУдалить
  10. Ты не понял.
    public Integer my() {
    Integer a = method1();
    Integer b = method2();
    return a + b;
    }
    let-checked прямо внутри метода умеет проверить, что a и b получили нормальные значения, а не null-ы, иначе a+b вычисляться не будет.

    ОтветитьУдалить
  11. Post Sharp: http://habreffect.ru/files/c75/94bb44510/PostSharpInAction.png

    Монады: http://habreffect.ru/files/886/c2acf48c8/MonadicSintax.png

    Как-то так..

    ОтветитьУдалить
  12. ...
    return person.IsNull().address.IsNull().postCode.IsNull(); -- содержит целых три проверки на нул.

    Короче, учи матчасть.

    ОтветитьУдалить
  13. Ммм. Параметры метода, помеченного атрибутом CheckNull, будут проверяться на null, и возвращается исключение, если хотя бы один из них null (в примере с PostSharp). Это, можно сказать, прямо внутри метода, так как метод OnEntry имеет доступ к окружению прослушиваемого метода через параметр MethodExecutionArgs.

    ОтветитьУдалить
  14. Цит.: "return person.IsNull().address.IsNull().postCode.IsNull(); -- содержит целых три проверки на нул."

    А как же по-другому то. Монады упрощают синтаксис путём организации цепочек, для иерархических структур - самое оно.

    ОтветитьУдалить
  15. Я описал способ изменить язык программирования таким образом, чтобы все, о чем ты пишешь, не нужно было вообще. Ни Exception-ы (которые надо еще отловить), ни ручные проверки на null. Я ввел в язык новые "ключевые слова", которые в твоем убогом C# заменили бы public void () {} и оператор "=". Только нельзя в C# изменить синтаксис языка, вот тебе и приходится кочевряжиться за счет аспектно-ориентированного программирования и паттернов. О паттернах, кстати, я отдельно напишу в следующих постах.

    ОтветитьУдалить
  16. Признаю, что лисп - один из самых функциональных языков, позволяет легко метапрограммировать, использовать истинные макрокоманды и т.д. Что он имеет и академическое, и практическое, и коммерческое применение. И думаю, что в один прекрасный день я "узрю" всю эту мощь, и начну применять. Но выбор свой делаю в пользу простого, универсального и востребованного (но никак не убогого) языка(ов), который пока позволял сделать всё, что мне требовалось.

    ОтветитьУдалить
  17. А я тут вот натолкнулся на http://fprog.ru/2010/issue4/alex-ott-clojure/ 3.5 Функции Контрактное программирование

    ОтветитьУдалить
  18. Да, в общем, тоже прикольно. Судя по всему, на Clojure можно решить эту проблему множеством способов; на Java/C#/C++ -- нет ни одного удовлетворительного.

    ОтветитьУдалить