Хотя сам занимаюсь разработкой на 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, и будет выполнено тело выражения.
А ваш язык программирования может такое?
Офигеть как удобно. Вместо того чтобы использовать систему типов с явной обнуляемостью значений будем городить (пусть и с помощью макросов) рантайм-проверки.
ОтветитьУдалитьВообще, наверное, это дело вкуса. Здесь я просто хотел показать, что clojure -- более гибкий инструмент, позволяющий автоматизировать то, что постоянно приходится писать вручную на java. Как ни крути, но на java будут все те же самые рантайм-проверки, только писать их будет не макрос, а программист.
ОтветитьУдалитьА как быть с проверкой на 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);
}
}
Такую многоуровневую проверку тоже несложно реализовать при помощи макросов, как-нибудь покажу на досуге. И тебе здесь не удастся меня подловить: я могу изменить синтаксис Clojure, а ты в своем C# -- нет :-)
ОтветитьУдалитьВ объектно-ориентированных языках уже достаточно давно придумана такая вещь, как Null Object Pattern. Последовательно используя этот приём, можно избавиться от большинства проверок на null без риска словить исключение.
ОтветитьУдалитьКонечно, этот шаблон подходит не во всех случаях (например, если мы пишем библиотеку, которую может использовать кто угодно, придётся подстраховаться). Но для большинства практических случаев он является удобным и элегантным решением.
Да, действительно, если мы сами делаем API, то мы можем разработать методы, которые никогда не вернут null. А что если я использую уже готовые сторонние решения, для которых null -- вполне нормальное возвращаемое значение?
ОтветитьУдалитьКроме того, реализовывать NullObject для каждого класса -- это еще больше лишнего кода, чем проверки на null.
Да ну. Null Patern для каждого класса - роскошь и не оправданная. К тому же, зачастую (да в большинстве реальных задач!) действительно нужно вернуть null.
ОтветитьУдалитьДругое дело - перейти к монадическому синтаксису или использовать возможности библиотеки PostSharp.
Посмотрел PostSharp. Честно говоря, не представляю, как с ее помощью реализовать хоть что-то похожее на синтаксическую конструкцию let-checked.
ОтветитьУдалитьЭтот комментарий был удален автором.
ОтветитьУдалитьТы не понял.
ОтветитьУдалитьpublic Integer my() {
Integer a = method1();
Integer b = method2();
return a + b;
}
let-checked прямо внутри метода умеет проверить, что a и b получили нормальные значения, а не null-ы, иначе a+b вычисляться не будет.
Post Sharp: http://habreffect.ru/files/c75/94bb44510/PostSharpInAction.png
ОтветитьУдалитьМонады: http://habreffect.ru/files/886/c2acf48c8/MonadicSintax.png
Как-то так..
...
ОтветитьУдалитьreturn person.IsNull().address.IsNull().postCode.IsNull(); -- содержит целых три проверки на нул.
Короче, учи матчасть.
Ммм. Параметры метода, помеченного атрибутом CheckNull, будут проверяться на null, и возвращается исключение, если хотя бы один из них null (в примере с PostSharp). Это, можно сказать, прямо внутри метода, так как метод OnEntry имеет доступ к окружению прослушиваемого метода через параметр MethodExecutionArgs.
ОтветитьУдалитьЦит.: "return person.IsNull().address.IsNull().postCode.IsNull(); -- содержит целых три проверки на нул."
ОтветитьУдалитьА как же по-другому то. Монады упрощают синтаксис путём организации цепочек, для иерархических структур - самое оно.
Я описал способ изменить язык программирования таким образом, чтобы все, о чем ты пишешь, не нужно было вообще. Ни Exception-ы (которые надо еще отловить), ни ручные проверки на null. Я ввел в язык новые "ключевые слова", которые в твоем убогом C# заменили бы public void () {} и оператор "=". Только нельзя в C# изменить синтаксис языка, вот тебе и приходится кочевряжиться за счет аспектно-ориентированного программирования и паттернов. О паттернах, кстати, я отдельно напишу в следующих постах.
ОтветитьУдалитьПризнаю, что лисп - один из самых функциональных языков, позволяет легко метапрограммировать, использовать истинные макрокоманды и т.д. Что он имеет и академическое, и практическое, и коммерческое применение. И думаю, что в один прекрасный день я "узрю" всю эту мощь, и начну применять. Но выбор свой делаю в пользу простого, универсального и востребованного (но никак не убогого) языка(ов), который пока позволял сделать всё, что мне требовалось.
ОтветитьУдалитьА я тут вот натолкнулся на http://fprog.ru/2010/issue4/alex-ott-clojure/ 3.5 Функции Контрактное программирование
ОтветитьУдалитьДа, в общем, тоже прикольно. Судя по всему, на Clojure можно решить эту проблему множеством способов; на Java/C#/C++ -- нет ни одного удовлетворительного.
ОтветитьУдалить