Страницы

Поиск по вопросам

Показаны сообщения с ярлыком дизайн-языка. Показать все сообщения
Показаны сообщения с ярлыком дизайн-языка. Показать все сообщения

воскресенье, 8 марта 2020 г.

Имплементация функционального интерфейса

#java #дизайн_языка #лямбда_выражение


Понимание интерфейса, как конструкции языка, до некоторых пор было мною вроде бы
как усвоено. Столкнувшись с лямбда-выражениями это понимание рискует быть пересмотренным.
Что я имею ввиду? 

Интерфейс позволяет "объединить" в некотором смысле относительно разные классы и
работать с ними под определенным углом (интерфейсом). Так или иначе, чтобы им (интерфейсом)
воспользоваться, его необходимо имплементировать.

Функциональные интерфейсы, которые определяют один метод, ведут себя иначе? Их не
нужно имплементировать, достаточно создать ссылку на них и присвоить этим ссылкам лямбда-выражение? 

Мне интересен сам масштаб возможностей, который здесь открывается. Выходит просто
ссылка на функциональный интерфейс внутри любого класса способна манипулировать объектами
любого класса?
    


Ответы

Ответ 1



Просто как пример, что же такое лямбда в Java Лямбда new AccidentsRequest(result -> {if (!result.has("error")) parseJSON(result);}, true); ТРАХ-ТИБИДОХ, АХАЛАЙ-МАХАЛАЙ!!! И лямбда превращается... В анонимный класс! new AccidentsRequest(new AsyncTaskCompleteListener() { @Override public void onTaskComplete(JSONObject result) throws JSONException { if (!result.has("error")) Content.this.parseJSON(result); } }, true); Фактически лямбды в Java - это такой синтаксический сахар для анонимных классов с одним методом.

среда, 26 февраля 2020 г.

LINQ в Go. Почему нет? [закрыт]

#golang #linq #дизайн_языка


        
             
                
                    
                        
                            Закрыт. На этот вопрос невозможно дать объективный ответ.
Ответы на него в данный момент не принимаются.
                            
                        
                    
                
                            
                                
                
                        
                            
                        
                    
                        
                            Хотите улучшить этот вопрос? Переформулируйте вопрос,
чтобы на него можно было дать ответ, основанный на фактах и цитатах, отредактировав его.
                        
                        Закрыт 8 месяцев назад.
                                                                                
           
                
        
У меня есть вопрос про linq. Почему в Go до сих пор нет в стандарте этой чудесной
штуки. Всё, что я слышал до сих пор по эту технологию от гоферов, так это то, что это
не по-гоферному. К сожалению, этот аргумент меркнет, когда тебе нужно сделать пару
группировок, а потом ещё пару проекций. В итоге, 3-4 строки трансформируются 3-4 экрана.
По-моему выбор очевиден. Да, есть проблемы. Например, для реализации LINQ, скорее всего
потребуется reflect, что приведёт к просаживанию производительности. Но при этом, не
стоит забывать, что с таким же успехом, можно сказать, что и сам reflect приводит к
тому, что производительность просаживается. Так давайте от него откажемся. Конечно,
этого никто не сделает. Вопрос в том, где корректно использовать ту или иную технологию.

Кстати говоря, аналог reflect есть. Это статическая генерация кода. Например, так
реализованы некоторые не стандартные пакеты в Go для работы с JSON.

Для ознакомления с технологией LINQ в го, привожу ссылку.



Я завёл issue в основной репе golang. Требуется достаточно весомое обоснование для
открытие такого issue.



Продолжая моё исследования linq для golang, я нашёл следующее. Здесь человек производит
сравнение koazee и обычных циклов. Легко заметить, что он приходит к выводу, что koazee
не сильно уступает нативному golang. Обсуждение здесь. К слову, скажу, koazee -- лучшая
реализация linq для golang. Лучшая == самая быстрая и функциональная на момент 09.07.2019.



Одна из неудобных вещей в golang и linq -- это способ обращения к лямбдам. Так как
в golang нет шаблонов, придётся писать конструкции, которые похожи на эти:

From(cars).Where(func(c interface{}) bool {
    return c.(Car).year >= 2015
}).Select(func(c interface{}) interface{} {
    return c.(Car).owner
})


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

В go-linq создателям удалось добиться эмуляции шаблонов:

From(cars).Where(func(c Car) bool {
    return c.year >= 2015
}).Select(func(c Car) string {
    return c.owner
})


При этом, скорее всего (если ничего не изменилось), go-linq много медленнее, koazee.

Взято отсюда. 09.07.2019



Ещё  одна библиотека, которая ничего не упоминает про linq, но реализует похожий
функционал. Не поддерживается, к сожалению.  09.07.2019
    


Ответы

Ответ 1



На этот вопрос вам точно ответят только в Go Team, но ответ скорее всего будет около следующего. Во-первых, для LINQ à la C# нужны обобщённые коллекции типа IEnumerable, которые если и появятся, то только в Go 2.0. На интерфейсах это всё пойдёт через отражение, что медленно, и местами небезопасно. Во-вторых, само название «Language Integrated Query» говорит, что нужна поддержка со стороны языка. Это означает новые ключевые слова, причём не одно. Учитывая, что Go Team крайне неохотно добавляет ключевые слова, их добавление крайне маловероятно. Не говоря уже о том, что это синтаксический сахар. В-третьих, даже если опустить синтаксис, для LINQ понадобятся анонимные функции с выведением типов, что является ещё одним расширением языка, и что вряд ли появится даже в Go 2.0.

четверг, 2 января 2020 г.

Перегрузка оператора < в c#

#c_sharp #дизайн_языка


У меня есть такой код:

public class Foo
{
    public static bool operator<(Foo l, Foo f)
    {
        Console.WriteLine("Foo!");
        return false;
    }
    //public static bool operator>(Foo l, Foo f)
    //{
    //    return f < l;
    //}
}


Я удивился, что в c# он не компилируется (сам пришел в с# после изучения с++). Соответсвенно
возник вопрос, почему компилятор требует "парную" реализацию операторов <, >. Например
в с++ такой код будет работать. Не могу понять причины такого дизайна в с#.
    


Ответы

Ответ 1



Потому что C#, в отличие от плюсов, заточен не на максимальную эффективность, а на максимальную предсказуемость. Он сам реализует операторы комбинированного присваивания, гарантируя, что a+=b будет работать так же, как a=a+b. И он требует чтобы парные операторы были реализованы в паре. Если я могу написать aa, причём, я буду ожидать, что эти условия эквивалентны. Не знаю, почему они не стали генерировать второй оператор сами, но причины его необходимости кажутся достаточно понятными - программист будет интуитивно ожидать, что второй ему тоже доступен.

Ответ 2



В документации сказано: При перегрузке операторов сравнения они должны перегружаться парами; то есть если оператор == перегружается, оператор != тоже должен перегружаться. Обратное также верно, и сказанное относится также к парам операторов < и >, <= и >=. Обратите внимание, что не все операторы могут быть перегружены. Кроме того, некоторые операторы имеют ограничения. +, -, !, ~, ++, --, true, false - эти унарные операторы могут быть перегружены. +, -, *, /, %, &, | , ^, <<, >> - эти бинарные операторы могут быть перегружены. ==, !=, <, >, <=, >= - операторы сравнения могут быть перегружены &&, || - условные логические операторы не могут быть перегружены, но они оцениваются с помощью & и | , которые могут быть перегружены. [] - оператор индексирования массива не может быть перегружен, но можно определить индексаторы. (T)x - оператор приведения типов не может быть перегружен, но можно определить новые операторы преобразования +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= - операторы присваивания не могут быть перегружены, но +=, например, оценивается с помощью +, который может быть перегружен. =, ., ?:, ??, ->, =>, f(x), as, checked, unchecked, default, delegate, is, new, sizeof, typeof - эти операторы не могут быть перегружены.

пятница, 27 декабря 2019 г.

Метод ForEach и IEnumerable

#c_sharp #net #linq #дизайн_языка


Подскажите, а в чем мотивация того, что этот метод работает только с List? Ведь
IEnumerable тоже имеет все необходимое.
    


Ответы

Ответ 1



Многие люди спрашивают меня, почему Microsoft не сделал для коллекций метод расширения ForEach. У класса List уже есть такой метод, но почему не добавить такой метод для всех последовательностей. Практически - это однострочник: public static void ForEach(this IEnumerable sequence, Action action) { // argument null checking omitted foreach(T item in sequence) action(item); } Обычно на вопрос: "Почему X не реализовано?" я отвечаю, что любая особенность не реализована, пока кто-то ее не спроектирует, реализует, протестирует, и предоставит. И никто не даст денег на это. И да, хотя я отмечал, что даже самые маленькие особенности могут иметь большую стоимость, конкретно эта по настоящему простая, очевидная, легко тестируемая и легко документируемая. Стоимость всегда имеет значение, но в этом случае она незначительна. С другой стороны, если это так просто, почему бы не сделать это самому, если так нужно? И по настоящему важна тут не низкая стоимость, а полученная выгода. Дальше будет видно, что я считаю выгоду здесь также небольшой, если вообще не отрицательной. Но мы должны посмотреть немного глубже. У меня есть две причины против этого метода. Первая причина заключается в том, что это нарушает принципы функционального программирования, на которых основаны остальные операторы для последовательностей. Очевидно, что цель этого метода - вызвать побочные эффекты. Цель выражения (expression) - вычислить значение, а не получить побочный эффект. Получение побочного эффекта - это цель для statement. Его вызов выглядит как выражение (expression) (хотя, надо признать, что, так как метод возвращает void, выражение может быть использовано только в контексте "statement expression"). Меня не устраивает делать единственный оператор только для последовательностей, который будет использоваться только для побочных эффектов. Вторая причина заключается в том, что это ничего не добавляет языку. И сделав это можно будет переписать совершенно чистую строку кода foreach(Foo foo in foos){ statement involving foo; } в следующую foos.ForEach((Foo foo)=>{ statement involving foo; }); которая использует почти те же символы в немного другом порядке. Кроме того, вторая версия тяжелее для понимания, отладки, вводит семантику замыканий, что в некоторых случаях может поменять время жизни объекта. Когда мы предоставляем два похожих способа сделать одно и то же, мы вносим путаницу в индустрии, становится тяжелее читать код друг друга и т.д. Иногда выгода от наличия двух разный текстовых представлений для одной операции (например, query-синтаксис vs method-синтаксис или + vs String.Concat) настолько огромна, что можно пренебречь потенциальной путаницей. Но убедительное преимущество query выражений - их читаемость; новая форма foreach читается определенно не лучше, а может быть даже и хуже. Если вы не согласны с такими философскими возражениями и видите практическую ценность в таком шаблоне, идите и напишите этот однострочник сами. Перевод блога @EricLippert

Ответ 2



Этот метод нужен только для side-эффектов, что противоречит функциональной концепции linq. Этот метод не ведёт к сокращению кода: foreach(Foo foo in foos){ statement involving foo; } foos.ForEach((Foo foo)=>{ statement involving foo; }); получились почти те же символы, но в несколько другом порядке. К тому же вторую версию сложнее понять, сложнее отладить и в ней появляется замыкание, которое может повлиять на время жизни объектов. Но каждый, кто не согласен с этими философскими причинами и видит пользу в этом паттерне, - вперёд! Просто реализуйте этот тривиальный однострочник самостоятельно. Источник: https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/

Ответ 3



До .net 4.5 разница была во внутренней реализации: foreach внутри реализовался(и реализуется) через итератор. Это означает что все завалится как только убрать из списка один элемент. Зато работает быстрее. ForEach() же БЫЛ внутри реализован через for(int i=0; i { if(x.RemoveMe) someList.Remove(x); }); Начиная с .net 4.5 оба метода работают через итератор и являются взаимозаменяемыми. Просто разный синтаксис использования. Информация взята с: https://stackoverflow.com/a/226082/4423545 Так же нашлась статья Eric Lipperts blog "foreach" vs "ForEach" -- а он -- principal developer on the C# compiler team. Думаю, что там можно нарыть какую-то дополнительную информацию. На сколько я понял, ForEach оставили только листу и не привязывали всем IEnumerable что бы не плодить: foos.ForEach((Foo foo) => { statement involving foo; }); // сложно для понимания при беглом взгляде а что бы люди писали: foreach(Foo foo in foos) { statement involving foo; }// легко для понимания И даже в 1 строку читается все равно проще: foreach(Foo foo in foos){ statement involving foo; }

вторник, 24 декабря 2019 г.

Зачем нужен __iter__, когда есть __next__?

#python #python_3x #классы #итераторы #дизайн_языка


Я учу python и у меня возник вопрос по итераторам.
Чтобы можно было итерироваться по объекту, у него должен быть метод __iter__, который
вернет объект-итератор.
У объекта-итератора должен быть метод __next__, который будет возвращать следующий
элемент из объекта, по которому итерируемся, либо кидать ошибку StopIteration.
Я встречал только такой код, в котором __iter__ возвращает self.
Например 

class RandomIterator:
    def __iter__(self):
        return self

    def __init__(self, k):
        self.k = k
        self.i = 0

    def __next__(self):
        if self.i < self.k:
            self.i += 1
            return random()
        else:
            raise StopIteration


Вот у меня и возник вопрос, какой смысл от метода __iter__?
    


Ответы

Ответ 1



__iter__() в Python Как вы узнали из урока «Классы и объекты Python», у всех классов есть функция под названием __init__(), которая позволяет вам делать инициализацию при создании объекта. Метод __iter__() действует аналогично, вы можете выполнять операции (инициализацию и т. Д.), Но всегда должны возвращать сам объект итератора. немного хабра Теперь, когда речь зашла о создании собственных последовательностей в Питоне, пришло время поговорить о протоколах. Протоколы немного похожи на интерфейсы в других языках тем, что они предоставляют набор методов, которые вы должны реализовать. Однако, в Питоне протоколы абсолютно ни к чему не обязывают и не требуют обязательно реализовать какое-либо объявление. Наверное, они больше похожи на руководящие указания. Почему мы заговорили о протоколах? Потому, что реализация произвольных контейнерных типов в Питоне влечёт за собой использование некоторых из них. Во-первых, протокол для определения неизменяемых контейнеров: чтобы создать неизменяемый контейнер, вы должны только определить __len__ и __getitem__ (продробнее о них дальше). Протокол изменяемого контейнера требует того же, что и неизменяемого контейнера, плюс __setitem__ и __delitem__. И, наконец, если вы хотите, чтобы ваши объекты можно было перебирать итерацией, вы должны определить __iter__, который возвращает итератор. Этот итератор должен соответствовать протоколу итератора, который требует методов __iter__(возвращает самого себя) и next. __iter__(self) Должен вернуть итератор для контейнера. Итераторы возвращаются в множестве ситуаций, главным образом для встроенной функции iter() и в случае перебора элементов контейнера выражением for x in container:. Итераторы сами по себе объекты и они тоже должны определять метод __iter__, который возвращает self. UPDATE (вернул пример с кодом, ибо попросили в комментах): Вот в примере по 1-й ссылке есть один из вариантов зачем. Когда вам нужно считать кол-во элеметов обработанных или иные связанные операции. Это полезно для уменьшения кол-во дублирования кода (см. принципы языка Python)iter() в Python class MyNumbers: def __iter__(self): self.a = 1 return self def __next__(self): x = self.a self.a += 1 return x А вот ниже мой пример использования __iter__ class Fibbonachi: def __iter__(self): self.cur_val = 0 self.next_val = 1 return self def __next__(self): tmp = self.next_val self.next_val += self.cur_val self.cur_val = tmp return tmp for i in Fibbonachi(): print(i) if i > 100: break # Результат: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 в колонку

понедельник, 23 декабря 2019 г.

Почему интерпретатор питона не проверяет type hints?

#python #python_3x #language_lawyer #дизайн_языка #типизация


Здравствуйте! В питоне есть type hint`ы, однако код вроде такого:

def add(a: int, b: int) -> int:
    return a + b

add('hello', 'world')


отлично выполняется. Я знаю, что для проверки типов есть внешние утилиты, но не понимаю:
почему синтаксис в язык добавили, а проверки из коробки нет? 

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

Что-то такое, кажется, не сложнее парсить:

#: int, int -> int
def add(a, b):
    return a + b

#: [int]
li = [1,2,3]


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


Ответы

Ответ 1



Type hint'ы введены PEP 484. Держите под рукой. почему синтаксис в язык добавили, а проверки из коробки нет? Вообще на любые вопросы "почему в языке Х нет фичи Y", при отсутствии явного отказа от неё, ответ "не сделали ещё, может быть когда-нибудь". Но в PEP 484 есть предложение, очень похожее на отказ делать это частью языка: It should also be emphasized that Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention. Важно также отметить, что Python останется динамически типизированным языком, и его авторы не намереваются когда-либо делать type hint'ы обязательными, даже если лишь по соглашению. Хотя это напрямую не исключает возможности появления необязательных проверок в стандартной библиотеке, это стало бы существенным шагом в сторону "статической типизации по соглашению", чего авторы языка не хотят. Как бы там ни было, type hint'ы должны стабилизироваться в языке, прежде чем основывать на них такую крупную фичу, как встроеннные проверки. А это случится не раньше Python 3.8. Ведь внешние утилиты могут проверять без какого либо дополнительного синтаксиса Да, но: Каждый анализатор вынужден для этого вводить собственный синтаксис. Возможно, когда-нибудь разные реализации пришли бы к общему де-факто стандарту. А можно сделать "превентивный стандарт", чтобы исключить другие развития событий на корню. Этот синтаксис должен затруднять написание аннотаций, несовместимых с описываемым кодом. Комментарии очень легко сделать несовместимыми с кодом, чтобы это не случилось, нужны дополнительные проверки самого кода. Разбирать сам код на Python разумно парсером Python, а не создавать и поддерживать его кусочек по новой. И если уж заставлять этим заниматься парсер Python, можно интегрировать синтаксис плотнее в язык, например указывать тип аргумента непосредственно рядом с ним. Что уже сделано аннотациями в PEP 3107, но аннотации постепенно становятся только type hint'ами и больше ничем.

среда, 18 декабря 2019 г.

Написание языка программирования: с чего начать? [закрыт]

#дизайн_языка


        
             
                
                    
                        
                            Закрыт. Этот вопрос не по теме. Ответы на него в данный
момент не принимаются.
                            
                        
                    
                
                            
                                
                
                        
                            
                        
                    
                        
                            Хотите улучшить этот вопрос? Update the question so it's
on-topic for Stack Overflow на русском.
                        
                        Закрыт 4 года назад.
                                                                                
           
                
        
Я далеко не ас, но хочется попробовать написать какой-то простой язык программирования
для веба. Никакой мании величия, просто хочется попробовать. Подскажите, с чего можно
начать?
    


Ответы

Ответ 1



Если хотите создавать язык для веб, очевидно будете писать инструмент для интерпретатции/компиляции приложений получающих данные от сервера и выдающих текст в поток стандартного вывода, с тем, чтобы иметь возможность выводить результаты работы программы (веб страницу, например) - здесь могу посоветовать почитать о технологии CGI, написать парочку простых CGI-скриптов (на чем угодно)... Определитесь, какой язык будете создавать - интерпретируемый или компилируемый. Каков будет результат работы "компилятора" (например, Вы можете просто написать транслятор, который будет переводить программу на ВАШЕМ языке в эквивалент на PHP, который и будет в дальнейшем использоваться). Наконец, по синтаксическому анализу, компиляции и прочему - советую (не в первый раз) - Дж. Креншоу, "Давайте создадим компилятор" - для человека, который не собирается заморачиваться теорией формальных языков, обратной польской записью, формами Бэкуса-Наура и пр... пр... пр... - в самый раз. Если хотите серьезно заниматься компиляторами - А.Ахо "Компиляторы: принципы, технологии и инструменты" ("Dragon book")... P.S. Если интересуетесь скриптами (в частности, для игр) и скриптовыми языками, советую также обратить внимание на Alex Varanese "Game Scripting Mastery" - хорошо написана и легко читается (на мой вкус)

Ответ 2



Язык программирования — просто его идея, спецификация описывающая синтаксис, семантику и стандартную библиотеку. Реализация языка программирования — программа, которая траслирует программу из исходного текста этого языка в код какого-либо другого выходного языка. При этом выходной язык может быть ассемблер, байткод виртуальной машины или любой другой язык. Реализация языка программировая — программа которая получает самый обычный текст, преобразует и выводит результат преобразования (текстовый или бинарный). Т.е. здесь нет никакой магии. Сам язык программирования придумать можно и не написав ни строчки кода (хотя и чертовски сложно, ведь код можно тестировать). То чего хотите вы — написание реализации языка программирования. Программу которая переводит текст вида print('test') в код Вполне можно считать примитивным вариантом транслятора. Руководствуясь материалами указанными другими участниками можно сделать гораздо более сложный пример.

Ответ 3



Рекомендую: Свердлов - "Языки программирования и методы трансляции" В книге приведены исходники компилятора упрощённого варианта Oberon на нескольких языках (Java, C++, Delphi). В качестве таргета реализована своя стековая виртуальная машина на байт-коде(как это делается в Java и .NET) (приведены исходники исполнителя и ассемблера). Книга понравилась именно практической направленностью.

Ответ 4



Начните с написания примеров маленьких программок (для начала "Hello World !") на Вашем языке, сопровождаемых человеческими описаниями, что, как и для чего эта программка делает. Входные данные, поток событий и что видим в результате. Потом станет понятно как разбирать синтаксис (и какой наиболее удобный), какие промежуточные формы строить, как исполнять - интерпретировать (и на чем) или во что компилировать.

Ответ 5



Изучайте ассемблер. И вообще скриптовые языки появились относительно недавно, поэтому они волей неволей имеют своих предков большой тройки С, Pascal, Java. Так что если создавать что-то новое в скриптовом направлении, то без них вряд ли удастся обойтись. А если хотите создать какой-либо язык, то пишите на Ассемблере - это язык машин - и это уже фаза низкоуровневого программирования. А пхп с и др. - это высокоуровневое программирование. То есть мы написали прогу на Сишке, компилятор перегнал ее на Ассемблер и машина поняла нашу прогу. Так что Ассемблер вам подойдет.

Ответ 6



Чтобы написать другой язык программирования надо использовать какой-то язык всеравно. Например возьмем php. mylang.php ".$text.""; } function sql($sql){ $colsql = 0; $sql = mysql_query($sql) or die (mysql_error()); $colsql+; } function text($text){ print $text; } ?> index.php Сделано'.$colsql.' запроса в базу данных.'); ?> ?>

воскресенье, 15 декабря 2019 г.

Почему приоритет постфиксного инкремента больше чем префиксного?

#cpp #language_lawyer #дизайн_языка


Оказалось, что постфиксные инкремент и декремент имеют более высокий приоритет, чем
префиксные (источник). Правда, там же есть интересная фраза, смысл которой я не очень
понимаю: "Стандарт не определяет порядок приоритетов. Они выводятся из грамматики языка."

Если посмотреть на запись вида ++x++ и начать выбирать, что же первым хотел использовать
программист, то возникает желание поменять эти приоритеты местами и сначала выполнить
префиксный. Логика простая: каждый из этих операторов требует lvalue, но только префиксный
возвращает lvalue. Получается, вычисление первым постфиксного оператора даёт rvalue
и префиксный уже неприменим.

Что я упускаю, и почему приоритеты выбраны именно так?

Не вижу никакого вреда для совместимости с Си, если приоритеты расставить вот так:



PS: На основе вопроса.
    


Ответы

Ответ 1



Языки С и С++ - разные, но родственные языки, имеющие общие исторические корни и очень схожий фундаментальный синтаксис. Основа грамматики языка С++ пришла из языка С, где так же как и в С++, все постфиксные операторы ([], (), ++, -> и т.п.) имеют более высокий приоритет, чем все префиксные (*, -, ++ и т.п.) Ни в языке С, ни в языке С++ нет и никогда не было некоего индивидуального "расставления приоритетов" операторов. Операторы фигурируют в грамматике сразу целыми классами терминальных символов. В данном случае это именно классы постфиксных и префиксных (aka унарных) операторов. При этом в языке С ваша логика неприменима. Одним из фундаментальных отличий языка С от языка С++ является то, что язык С не старается сохранить lvalue-ность результатов операторов без явной на то необходимости. В языке С и постфиксный, и префиксный ++ возвращает rvalue. Поэтому ваш вопрос фактически превращается в вопрос о том, почему грамматика С++ не была переделана на основе приведенных вами соображений о том, что в С++ префиксная форма ++ возвращает lvalue. Ответ: потому что приведенные вам соображения не достаточны в качестве основания для такой переделки. Негативный эффект от усложнения грамматики был бы заметен, а позитивный - ничтожен. И это все только ради возможности писать ++x++ вместо (++x)++?

Ответ 2



Так легче для восприятия. Посмотри на таблицу приоритетов, сначала идут все постфиксные операторы, потом все префиксные, потом все бинарные. Убедительной причины делать исключения для ++ и -- не было. Нужно буть немного не здоровым, чтобы писать в коде ++x++.

Зачем Apple придумала язык программирования Swift?

#objective_c #swift #apple #дизайн_языка


Чем не устроил их Objective-C? Какие преимущества у Swift перед Objective-C?     


Ответы

Ответ 1



Swift — гораздо более современный язык, не отягощённый проблемами совместимости с C. Добавить фичи наподобие безопасности памяти (memory safety), обобщённых классов/методов (generics), необнуляемых ссылок (non-nullable reference types) было бы очень сложно, оставаясь в рамках Objective C. (Сравните, например, лямбды в Objective C и в Swift'е.) Синкатсис, да и семантика C хороши для низкоуровневых языков (каким, например, C и является), но высокоуровневые фичи проще делать на другой основе.

среда, 11 декабря 2019 г.

Почему имена встроенных функций в Go набраны строчными буквами?

#golang #дизайн_языка


Функции и переменные с именами начинающимися со сточной буквы не видны снаружи пакета,
так почему встроенные типы и функции начинаются со строчной буквы? Они описаны в пакете
main?
    


Ответы

Ответ 1



Имена встроенных функций не входят ни в один пакет, и в Go нет перегрузки функций, по этому если существует встроенная функция foo, то ни в одном пакете нельзя объявить функцию с именем foo. Если бы встроенные функции начинались с большой буквы, например Append или Close, то нельзя было бы сделать метод с таким именем, что весьма неудобно. Более того, если в будущем появится новая встроенная функция, например Move, то надо будет переписывать все пакеты, имеющие такую функцию. Более того, т.к. это публичное API, надо будет переписать все пакеты, в которых эта функция вызывается. По этому встроенные функции начинаются с маленькой буквы. Это не мешает использованию этих функций в публичном API, и при добавлении новых встроенных функций будет сломано меньше кода.

понедельник, 9 декабря 2019 г.

Что такое фантомный тип и как это связано с traits?

#scala #traits #дизайн_языка #теория_типов


Много раз натыкался на термин «фантомный тип», особенно в контексте обсуждения traits
в языке Scala.

Что это такое? При чём здесь traits?
    


Ответы

Ответ 1



Заранее прошу извинить за длину ответа, просто иначе понять, что такое фантомные типы и как они используются, будет трудно, а тема и правда интересная. Предположим, "Роскосмос" попросил написать нас систему управления запуском ракет. "Ракета должна быть заправлена топливом и кислородом, и только после этого можно запускать", - лаконично констатирует техническое задание. Не долго думая, мы пишем следующий код на языке Scala (в функциональном стиле, не в объектно-ориентированном, почему - будет видно впоследствии): object UnsafeRocketModule { case class Rocket private[UnsafeRocketModule] (hasFuel: Boolean, hasO2: Boolean) def createRocket() = Rocket(false, false) def addFuel(r: Rocket) = Rocket(true, r.hasO2) def addO2(r: Rocket) = Rocket(r.hasFuel, true) def launch(r: Rocket) = if (!r.hasFuel || !r.hasO2) throw new Exception("Попытка запустить неподготовленную ракету!") else println("3-2-1... Пуск!") } Модуль UnsafeRocketModule определяет тип Rocket, функцию-конструктор ракет (т.к. конструктор типа Rocket не экспортируется модулем в целях поддержания инкапсуляции), а также функции заправки ракеты топливом, кислородом, и функцию запуска ракеты. Мы также отслеживаем состояние ракеты (заправленность топливом и кислородом) и генерируем исключение при попытке запустить неподготовленную ракету. Загрузим этот модуль в Scala REPL, а затем попробуем ввести определение функции, которая подготавливает ракету (заправляет топливом и кислородом), и запускает ее: scala> def prepareAndLaunchRocket() { | import UnsafeRocketModule._ | launch(addFuel(createRocket())) | } prepareAndLaunchRocket: ()Unit Все скомпилировалось без ошибок, можно работать: scala> prepareAndLaunchRocket() java.lang.Exception: Попытка запустить неподготовленную ракету! at UnsafeRocketModule$.launch(:13) at $anonfun$prepareAndLaunchRocket$2.apply(:8) at $anonfun$prepareAndLaunchRocket$2.apply(:8) ... пропущено ... Вот незадача: в тексте функции prepareAndLaunchRocket мы заправили ракету топливом, но забыли заправить кислородом! Эта типичная для программ ошибка: попытка выполнить операцию над объектом, который находится в неподходящем для этого состоянии. Этот класс ошибок выявляется во время выполнения программы (run time), и очень часто, уже в процессе эксплуатации. Но мы ведь не хотим, чтобы из-за нашей программы взрывались ракеты, только потому, что в коде мы забыли подготовить ее должным образом, а из-за нехватки времени и внимания написали тесты, не покрывающие этот случай, что не позволило выявить проблему до сдачи в эксплуатацию. Поэтому, исправив функцию prepareAndLaunchRocket, мы понимаем, что пришло время что-то кардинально менять. А именно: мы должны придумать способ гарантировать, что перед запуском (то есть, вызовом функции launch()) ракета будет подготовлена должным образом, то есть будут вызваны обе функции addFuel И addO2. Такая гарантия означает, что попытка запуска неподготовленной ракеты должна теперь выявляться во время компиляции (compile time) программы! Иными словами, компилятор не должен нам позволить скомпилировать ошибочное определение prepareAndLaunchRocket, приведенное выше. А это означает, что это определение не должно пройти проверку типов. То есть, мы должны расширить систему типов, принятую в нашем языке программирования. С этого момента начинается магия. Первое, что мы делаем, это переписываем определение типа Rocket следующим образом: object SafeRocketModule { case class Rocket[Fuel, O2] private[SafeRocketModule] () // ... } Что мы поменяли? Тип Rocket приобрел два параметра типа (Fuel и O2), а атрибуты hasFuel и hasO2 были изъяты. Мы перенесли отслеживание состояния заправленности ракеты топливом и кислородом из системы времени выполнения в систему типов, то есть в систему времени компиляции. То, что отслеживалось атрибутами класса отныне отслеживается параметрами этого типа. Теперь добавим следующие вспомогательные типы: sealed trait NoFuel sealed trait HasFuel sealed trait NoO2 sealed trait HasO2 Эти типы будут использоваться нами как значения времени компиляции вместо значений времени выполнения (true и false для атрибутов hasFuel и hasO2) для индикации состояния заправленности ракеты. Это типы-маркеры, существующие только для подстройки системы типов, они не имеют атрибутов, а их значения (new NoFuel { ... }) нами никогда не будут использованы. Такие типы называются фантомными. А traits являются удобным механизмом их определения. С их помощью мы можем переписать оставшиеся функции модуля: def createRocket() = Rocket[NoFuel, NoO2]() def addFuel[O2](r: Rocket[NoFuel,O2]) = Rocket[HasFuel,O2]() def addO2[Fuel](r: Rocket[Fuel,NoO2]) = Rocket[Fuel,HasO2]() def launch(r: Rocket[HasFuel,HasO2]) = println("3-2-1... Пуск!") Обратим внимание: потребности в проверке заправленности ракеты функцией launch() больше нет. Система типов гарантирует, что launch() будет вызвана только для корректно подготовленной ракеты. Кроме того, становится понятно, почему нам следует использовать функциональный стиль, а не объектно-ориентированный. В последнем случае объект Rocket был бы создан единственный раз и функции addFuel, addO2 и launch вызывались бы впоследствии как его методы. Нам же необходимо менять тип ракеты при вызове соответствующих операций, и создавать значения этих типов с нуля, что и предполагает функциональный стиль. Загрузим модуль SafeRocketModule в Scala REPL, а затем снова попробуем ввести ошибочное определение функции prepareAndLaunchRocket: scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(createRocket())) | } :8: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] launch(addFuel(createRocket())) ^ Компилятор не дал ошибочному определению попасть в код программы из-за ошибки во время проверки типов. Исправим определение функции: scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(addO2(createRocket()))) | } prepareAndLaunchRocket: ()Unit scala> prepareAndLaunchRocket() 3-2-1... Пуск! Компиляция прошла успешно, пробный запуск осуществлен без ошибок. Теперь в эксплуатацию попадет гарантированно корректная версия этой функции, даже при отсутствии тестов - верификацию выполнила за нас система типов. Наша новая реализация имеет также и другие преимущества, по сравнению со старой: расширенная нами система типов гарантирует, что ракета будет заправлена топливом и кислородом только один раз, не допуская повторных вызовов функций заправки для ракеты, которая уже была заправлена: scala> addFuel(addFuel(createRocket)) :10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.HasFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,?] addFuel(addFuel(createRocket)) ^ scala> addO2(addO2(createRocket)) :10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] required: SafeRocketModule.Rocket[?,SafeRocketModule.NoO2] addO2(addO2(createRocket)) ^ Рассмотренный способ применения фантомных типов часто используется при проектировании публичных интерфейсов (API) в таких языках, как Haskell, особенно для сложных интерфейсов. В других языках, с менее развитой системой типов, эти приемы не используются вовсе. К примеру, в Java, насколько мне известно, все это не работает "из коробки", и надо делать уродливую "мумбу-юмбу", чтобы получить похожий результат. Также следует заметить, что это не единственная область применения фантомных типов. Более подробно о последних и о возможных областях применения можно почитать здесь (все источники англоязычные): Phantom Types In Haskell and Scala. Пример с ракетами был адаптирован из этой статьи, где параллельно приводится версия на Haskell. Phantom type (HaskellWiki) A Foundation for Embedded Languages by Morten Rhiger - основательная академическая публикация о фантомных типах и их применениях в Haskell

Зачем в .NET типы данных разделили на ссылочные и значимые?

#c_sharp #net #дизайн_языка


Зачем в .NET типы данных разделили на ссылочные и значимые? 
    


Ответы

Ответ 1



Различие между ссылочными типами и типами-значениями на самом деле семантическое. Оно не в том, выделяется ли память в куче или нет (язык имеет право любой из объектов располагать в любой памяти, и делает это: например, типы-значения, попавшие в замыкание, будут «подняты» до кучи). Различие состоит в семантике равенства и копирования. Давайте посмотрим на пример. Вот у вас есть число 5. Это число — одно и то же, вне зависимости от того, как вы его получили. Если вы увеличите 5 на 1, вы не измените при этом само число 5, вы просто получите новое число. Когда вы копируете число в другую переменную, эта новая переменная живёт своей жизнью, не зависящей от старого числа. Теперь, пусть у вас есть объект, допустим, СписокПокупок. Этот объект ведёт себя совсем по-другому. Если вы создадите новый СписокПокупок, он будет отличаться от уже существующего: вы можете добавить товар в новый список, а в старом он при этом не появится. С другой стороны, когда вы копируете ссылку на список покупок в другую переменную, то вы продолжаете при это работать с тем же списком. Иными словами: у объектов типа списка покупок есть индивидуальность. А у объектов типа числа такой индивидуальности нет, они ничем не отличаются друг от друга. Так вот: объекты, которые ведут себя как число, и называются объектами-значениями. А объекты с индивидуальностью называются ссылочными объектами. Все остальные технические подробности служат лишь для реализации этой самой различной семантики. Например, объекты-значения чаще всего располагаются в стеке потому, что их можно расположить там: ведь у них никому не интересен конкретный экземпляр, поэтому за пределы метода можно выдать копию. А значит, оригинал можно держать на стеке (так эффективнее). Дополнительная литература: Eric Lippert. The Stack Is An Implementation Detail: Part 1, Part 2.

суббота, 7 декабря 2019 г.

Опасно ли разворачивать foreach через using as

#c_sharp #инспекция_кода #foreach #language_lawyer #дизайн_языка


В C# цикл foreach разворачивается в нечто такое:

Container container = new Container();
Enumerator enumerator = container.GetEnumerator();

try
{
    while (enumerator.MoveNext())
    {
        var element = enumerator.Current;
        // содержимое foreach
    }
}
finally
{
    IDisposable disposable = enumerator as IDisposable;
    if (disposable != null)
        disposable.Dispose();
}


Насколько безопасно и корректно изменить эту реализацию на такую:

Container container = new Container();
Enumerator enumerator = container.GetEnumerator();

using (enumerator as IDisposable)
{
    while (enumerator.MoveNext())
    {
        var element = enumerator.Current;
        // содержимое foreach
    }
}


Понятно, что для reference-типов никакой разницы нет.

Но если энумератор окажется value-типом, то при приведении к IDisposable он упаковывается,
что означает копирование структуры. Значит мы уничтожаем не ту же структуру, которую
использовали. На этом моменте и возникает различие между приведёнными вариантами: в
оригинальном структура скопирована после использования, а в модифицированном - перед.

Может ли такое различие привести к опасным последствиям? Если да, то к каким?
    


Ответы

Ответ 1



Фактически, ваш вопрос сводится к тому, что надо придумать непротиворечивый и не надуманный вариант реализации Enumerator причём такой, чтобы была реальная необходимость реализации в нём Dispose в процессе итерирования данные, которые в нём хранятся, менялись Сделав над собой героическое усилие и заставив себя оставить в тылу мысль о том, что "так" делать нельзя никогда, попробуем. Для того, чтобы обосновать необходимость реализации IDisposable можно представить себе итератор, который возвращает данные через какой-нибудь неуправляемой handle. Подойдёт, например, файл. public struct LineEnumerator : IEnumerator { private System.IO.FileStream _file; public LineEnumerator(string path) { _file = System.IO.File.OpenRead(path); } IDisposable.Dispose() { if (_file != null) { _file.Dispose(); _file = null; } } ... } После того, как мы непоправимо испортили себе карму таким итератором, выполнить второе условие очень легко. Представьте итератор, который смотрит не на файл, а на папку и по мере итерирования открывает файлы в указанной папке последовательно. Кончаются данные в одном файле, он закрывает его и открывает следующий. Таким образом, в процессе итерирования handle периодически меняется. public struct LineEnumerator : IEnumerator { private string _directoryPath; private System.IO.FileStream _file; public LineEnumerator(string directoryPath) { _directoryPath = directoryPath; } public bool MoveNext() { if (CheckEOF(_file)) { OpenNextFile(_directoryPath, ref _file); } ... } IDisposable.Dispose() { if (_file != null) { _file.Dispose(); _file = null; } } ... } Заблаговременно делать копию с такого итератора уже нельзя, если мы не хотим потерять handle на файл. Отсюда мораль: нет ни одной причины реализовывать такой нумератор как struct. Я бы даже сказал, что нет ни одного оправдания использованию struct по сравнению с class в данном случае.

Ответ 2



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

Локальная константа времени выполнения

#c_sharp #константа #дизайн_языка


В C++ для определения локальной константы времени выполнения можно написать так:

const auto c = f();


Далее, все попытки изменить c будут приводить к ошибке компиляции.

В C# такой возможности нет. Можно использовать readonly член подобным образом, но
не локальную константу. 

Почему существует такое ограничение (в чём причина отсутствия локальных констант
а-ля C++) и какое каноническое решение имеется для обеспечения локальной константности
времени выполнения на C#? Неужели для этого надо создавать отдельный read-only интерфейс?
    


Ответы

Ответ 1



Стоит отметить, что по локальным readonly-переменным есть официальный proposal. Фича опоздала к C# 7, но у нее все шансы войти в C# 8. Со стороны CLR для реализации нет никаких ограничений - readonly locals, как и все фичи C# со времен .NET 4.0 - это compile-time фичи - т.к. версия рантайма с тех пор не поменялась. По сути, proposal сводится к нескольким пунктам: возможности помечать параметры как readonly возможности помечать переменные как readonly шорткату let - эквиваленту readonly var Локальные readonly, как и неизменяемые foreach iteration variable - это, прежде всего, защита от дурака/ошибки копипасты. И уже потом - способ указать компилятору на возможную оптимизацию замыканий. На мой взгляд, сама по себе возможность вручную приписывать readonly к локальным переменным только ради в качестве защиты от потенциальной ошибки - неудобна для реального использования. Достаточно вспомнить о final в Java. Поэтому без шортката let фича будет достаточно бесполезной - ее будут использовать фанаты "безопасного кода" - те, кто сейчас принципиально не использует var и явно вписывают типы во всех упоминаниях генерика. Остальные пойдут по пути наименьшего сопротивления - гораздо дешевле и проще исправить одну ошибку раз в месяц, чем приписывать readonly к каждой переменной. Ну и опять же, тесты, при правильном применении, решают проблему ошибок копипасты. Более надежный способ защиты реализован, например, в F#, где локальные значения по умолчанию неизменяемы, а = вне объявления значения вообще не работает как оператор присвоения. let x = 1 x = 2 // This expression should have type 'unit', but has type 'bool' Для изменения значения разработчику придется сделать дополнительные телодвижения, как в строчке где переменная объявлена, так и в строчке, где переменная изменяется. let mutable x = 1 x <- 2 Сам язык подталкивает его к тому, чтобы объявить еще одно значение, а не менять существующее. Одна из трех добродетелей программиста - лень - уберегает его от ошибки. А просто так дописывать readonly к переменным, "как бы чего не вышло" - никто, кроме особых фанатов, не будет - из соображений той же лени.

Ответ 2



Ответы: JaredPar, и Jon Skeet Одна из причин - отсутствие поддержки CLR для локальных переменных только для чтения. Readonly переводится в CLR/CLI initonly опкод. Этот флаг может быть применен только к полям и не имеет смысла для локальных переменных. Фактически, применение этого кода к локальным переменным скорее всего сделает код непроверяемым. Это не значит, что C# не может реализовать это. Но это добавляет два смысла для одной и той же языковой конструкции. Версия для локальных переменных не будет иметь в CLR эквивалентного отображения. Обращаясь к ответу Jared'а, это возможно может быть compile-time фичей - компилятор будет запрещать тебе писать в переменную после первоначального объявления (которое должно включать присвоение). Есть ли в этом польза? Потенциальная - но не большая, если быть честным. Если ты не можешь сказать будет или нет присваивание еще где-то в методе, то твой метод слишком большой. Как бы то ни было, в Java есть эта особенность (при использовании модификатора final) и я очень редко видел его использование в случаях отличных от разрешения использования переменной в внутреннем анонимном классе - и где это было использовано это давало скорее впечатление беспорядка, чем полезной информации. в качестве обходного пути, если очень хочется, только на свой страх и риск есть такой: Настолько отличный, насколько ужасный пример В качестве run-time константы можно использовать переменную в блоке итератора: public void f() { foreach (int n in new int[] { getNum() }) // n declared constant { n = 3; // won't compile: "error CS1656: Cannot assign to 'n' because it is a 'foreach iteration variable'" } }

четверг, 5 декабря 2019 г.

Почему в .Net много запечатаных классов?

#c_sharp #net #clr #дизайн_языка


По каким причинам большинство .net классов являются запечатанными (например: Int32,
Double, String и т. п.)?

Есть ли в каких книгах/статьях объяснение данного архитектурного решения от создателей
платформы?
    


Ответы

Ответ 1



Все классы, которые не поддерживают наследование от них, должны быть sealed. Чтобы класс поддерживал наследование от него, необходимо обдуманное архитектурное решение: нужно рассмотреть реальные сценарии, когда это требуется; продумать, какие члена будут перегружаться; как остальные части системы будут работать с унаследованными классами и так далее. Если все классы сначала делать незапечатанными, а потом думать, то велика вероятность, что от этого будет больше вреда, чем пользы: никакие реальные возможности добавлены не будут, программисты полезут в потроха класса и нагромоздят хаков. И все эти хаки придётся поддерживать, потому что обратная совместимость — это святое. Это значит, что реализация может быть закрыта для улучшений в будущих версиях. Если какой-то класс закрыт, это значит, что архитектор не нашёл нормальных сценариев наследования, которые оправдывали бы незапечатенность класса. Классы типа String и StringBuilder закрыты, потому что это открывает возможности для изменения реализации классов, производительность которых критична для системы. Например, раньше билдер работал по принципу List, теперь он реализован через связный список блоков, каждый из которых растёт как List. Если бы хоть одна деталь старой реализации просочилась в protected интерфейс, изменить её было бы невозможно. Если бы внутренняя реализация была закрыта, то наследование было бы абсолютно бесполезно: все операции и так были бы публичными. В этом случае достаточно статических методов (в том числе extension методов). Что касается Int32 и Double, то это вообще-то структуры, они вообще не поддерживают наследование. Логика разработчиков .NET описана в книге Framework Design Guidelines. Некоторые части книги доступны на MSDN. На этот вопрос отвечал Эрик Липперт в статье Why Are So Many Of The Framework Classes Sealed? Он называет один ключевой принцип: "Хороший код делает ровно то, для чего он создан, ни больше ни меньше", а затем раскрывает его: Философский. Наследование основано на отношении is-a. Если нельзя придумать такой класс, то наследование запрещено. Практический. Создавать классы, которые можно расширять, сложно. Совместимость. Закрытый класс можно сделать открытым, но не наоборот. Безопасность. Если с помощью наследования можно переопределить логику базового класса, можно исковеркать логику и подвергнуть систему угрозе. Более развёрнутая аргументация по ссылке выше. В целом, классы во фреймворке "по умолчанию" закрыты примерно по той же причине, почему методы в C# по умолчанию невиртуальны. Можно вспомнить Java с виртуальными по умолчанию методами и учиться на ошибках.

среда, 4 декабря 2019 г.

Язык программирования без null

#null #дизайн_языка #типизация


Энтони Хоар, человек который ввёл в употребление NULL-указатель высказал следующую мысль:

I call it my billion-dollar mistake. It was the invention of the null reference in
1965. At that time, I was designing the first comprehensive type system for references
in an object oriented language (ALGOL W). My goal was to ensure that all use of references
should be absolutely safe, with checking performed automatically by the compiler. But
I couldn't resist the temptation to put in a null reference, simply because it was
so easy to implement. This has led to innumerable errors, vulnerabilities, and system
crashes, which have probably caused a billion dollars of pain and damage in the last
forty years.

Простите, что не по-русски, не нашёл качественного перевода.
Суть в том, что он считает введение NULL ошибкой, которая стоила многих сил, и что
вместо введения NULL необходимо было ввести дополнительные проверки во время компиляции.
Мне интересно, возможна ли жизнь без NULL, и как это особенно согласуется с динамическими
языками (быть может для языков со статической типизацей этого и можно было избежать,
а для динамических -- нет?). Вообще, как много есть языков, которые обходятся без NULL
или эквивалента?
Как минимум один мне известен: Haskell.
UPD:


Для языков со статической типизацией можно проанализировать код и увидеть все ли
переменные инициализированны. (Попробовать обойтись без NULL)
Для динамических языков (не только с динамической типизацией, а таких, где есть eval
или похожий инструмент) такого гарантированно сделать нельзя.

    


Ответы

Ответ 1



Проблема заключается не в самом null, его использование абсолютно легально, т.к. является по сути не артефактом конкретного языка программирования, а вычислительным приемом, паттерном, - общим для всей теории программирования. Всегда существуют вычислительные процессы (функции), работу которых можно оценивать с двух позиций: 1) есть результат; 2) нет результата. C этой точки зрения значение null есть унифицированный способ кодирования ситуации "нет результата". Проблема заключается в способе интеграции этого паттерна в систему типов языка. Значение null в большинстве языков не имеет типа, точнее, null является значением некоторого специального типа, являющегося подтипом ВСЕХ типов нашего языка. Поэтому null может быть числом, строкой, кнопкой пользовательского интерфейса, и вообще принимать любую форму. По сути дела, null - это "хак", непонятно как вписавшийся в статическую типизацию артефакт динамической типизации. Именно здесь начинаются проблемы, о которых пишет Хоар, и с которыми я согласен на 200%. Такой способ интеграции значения null означает, что в ЛЮБОМ месте, где мы ожидаем некоторое значение, мы можем получить null. И для нас как программистов нет способа гарантированно узнать, получим ли мы его или нет, а для компилятора гарантированно проверить, что в коде мы учли ту ситуацию, когда вместо ожидаемого результата получен null. Ситуацию могли бы несколько поправить (но не спасти!) хорошая документация и дисциплинированность программистов. Если бы не тот факт, что именно этих "добродетелей" в реальности почти не встретишь. Чтобы исправить проблемы null, нужно выполнить два условия: программисты должны быть лишены возможности использовать null в тех местах, где ЯВНО не объявили такую возможность; компилятор должен проверять и гарантировать нам, что клиентский код учел все такие ситуации, где вместо результата может быть получен null. Это можно сделать одним способом: сделать null значением некоторого обычного типа. В Haskell этим типом является Maybe, в Scala - Option, в F# - option. Значения null для этих языков называются соответственно Nothing, None и снова None. Тогда все встает на свои места: Выполнение 1-го условия: // Ошибка компиляции: мы не указали ЯВНО, что можно использовать None def notLessThan5(x: Int): Int = if (x >= 5) x else None // OK def notLessThan5(x: Int): Option[Int] = if (x >= 5) Some(x) else None Выполнение 2-го условия: // Ошибка компиляции: мы не учли, что notLessThan5() может не вернуть результата (вернуть None) println(notLessThan5(10) + notLessThan5(3)) // OK println(notLessThan5(10).getOrElse(5) + notLessThan5(3).getOrElse(5)) // напечатает 15 Во всех случаях соблюдение правильных принципов работы со значениями null проверит за нас компилятор, не допустив, чтобы NullPointerException "всплыла" в самый неподходящий момент во время эксплуатации программы. Проблемы null решены. Следует особенно заметить: мы не отказываемся от использования null. Мы просто интегрируем этот null в систему типов иначе, чем это сделано сейчас, в Java и им подобных. None, Nothing и проч. - есть точные эквиваленты null, только интегрированные в систему типов. Динамические языки программирования не используют преимущества статической типизации, поэтому все, что описано выше, их не касается. Избежать тех проблем, о которых говорил Хоар, в динамических языках нельзя в принципе - любая функция может вернуть ЛЮБОЕ значение. Это касается не только null, но вообще любых значений. Поэтому умные люди, которые используют динамические языки в повседневной практике, давно уже описали в литературе защитные практики от гибкости динамических языков. Основной практикой, помимо дисциплины и документации, является повсеместное и всеобъемлющее тестирование кода, причем как можно раньше, в идеале, до написания самого кода (TDD). Чудес не бывает: верификацию при проверке типов приходится заменять верификацией при тестировании. На эту тему рекомендую послушать великолепный доклад Robert Martin'а, произнесенный на RailsConf'09, "What Killed Smalltalk Could Kill Ruby, Too".

Ответ 2



Если поставить себе такую цель - написать программу не использующую NULL, скажем на C++, то это вполне осуществимо. Так что, в общем случае без NULL на C++ жить можно. На C# сложнее будет это сделать, поскольку большинство методов в .NET могут вернуть null. И как минимум проверять на null придется. Но это, согласитесь, уже не проблема языка, а проблема библиотек. Были бы библиотеки не использующие null, и программы можно было бы писать без него. К тому же, можно создать классы-обертки для того чтобы они инкапсулировали в себе работу с NULL и для внешнего наблюдателя никаких нулов не будет. Правильные программы на C++ пишутся именно так. Не думаю, что эти рассуждения не верны для других языков.

Ответ 3



Для минимизации вероятности типовых ошибок следует просто использовать стандартные алгоритмические подходы. ArgumentNullException кошернее, чем NullReferenceException, так как с большей вероятностью возникает именно там, где ошибка, а не постфактум.

воскресенье, 1 декабря 2019 г.

Динамические свойства языка программирования как преимущество

#objective_c #теория #связывание_данных #дизайн_языка


Наверстываю упущенную теорию по парадигмам языков программирования.

В обучающих материалах по ObjC часто встречается восхищение динамичностью языка (динамическое
связывание, динамическая типизация...) с указанием на то что многое можно делать в
рантайме плюс описание соответствующих трюков. Читая документ со страницы 23 я понимаю
что это есть свойство языка, но не преимущество. Предполагаю, что сравнивая со статически
типизированными языками все те же трюки выполняются в них с тем же успехом, но не в
runtime a в compile-time. Раздел статьи Преимущества (равно как и другие статьи в сети)
свет на вопрос для меня не пролил и не дал понять в чем именно преимущество а не просто
отличие от парадигмы статической типизации. Прошу просветить и указать в чем именно
преимущество динамической типизации над статической.

UPDATE:
Если будут получены объемные ответы на вопрос о природе явления в целом, то с целью
оставить тред потомкам я добавлю метки всех языков соответствующих парадигме и уберу
акцент на objc из топика.

UPDATE 2:
Получены прекрасные всеобъемлющие ответы, в частности от @VladD. C целью аккумулирования
топиком общеобразовательной ценности хотелось бы читать мнение и других участников
форума. Заранее благодарен за передачу опыта и знаний! 
    


Ответы

Ответ 1



Если уж зашла речь о «динамичности языка» вообще, должен отметить, что отдельное понятие «динамического языка» лишено смысла. В основном, когда говорят о «динамическом языке», имеют в виду «язык с динамической системой типов», хотя существуют и другие «динамические» черты языка (первое, что приходит в голову — «динамическая область видимости»). Итак, что такое «динамическая система типов»? Определения в источниках наподобие Википедии говорят, что разница в том, присваивается ли тип переменной при объявлении (и остаётся ли таким навсегда) либо при присваивании (и меняется с каждым следующим присваиванием). Это, однако, слишком грубое описание существующего положения вещей. Куда честнее и точнее было бы говорить о богатстве системы типов, и о том, насколько полно она отображает семантику предметной области. Крайний пример — языки типа большинства ассемблеров, в которых существует лишь один тип — числовой. Это соответствует примерам «максимально динамической» системы типов: переменная может содержать число, указатель, структуру из двух байт и т. д. Примера противоположной крайности — языка, в котором каждый элемент программной семантики описывается отдельным типом — наверное не существует, но старые диалекты Паскаля довольно близки к этому. Полностью последовательным строго типизированным языком был бы язык, в котором, например, была бы невозможна ошибка при взятии квадратного корня из отрицательного числа: в языке был бы определён тип «неотрицательное действительное число», и этот тип был бы типом аргумента корня. (То же для например арксинуса.) На деле же все языки стоят где-то посередине между этими двумя крайностями. Языки, которые считаются «динамически типизированными» или «слабо типизированными» (в зависимости от того, одобряет или осуждает программист это явление), имеют меньшее количество типов, и операции между значениями с разной семантикой либо завершаются с ошибкой, либо приводят к бессмысленному результату. В качестве примера рассмотрим javascript. Его переменные, по существу, имеют лишь один тип — переменная :-) Вы можете вызвать любую операцию на переменной, вне зависимости от фактического содержимого этой переменной. Для некоторых операций (например, вызов функции) неправильное содержимое переменной вызовет исключение, для некоторых же (например, сложение) операция будет выполнена с результатом, неожиданным для программиста, который не во всех деталях проштудировал спецификацию языка. Языки наподобие C++ предоставляют более богатую систему типов: вы, например, не сможете «вызвать функцию» по переменной типа int. Таким образом, у операций в C++ гораздо меньше возможностей завершиться аварийно из-за несоответствия типов. Тем не менее, язык предоставляет вам возможность нарушить защиту: оператор приведения типов. Вы можете привести тип строки к типу числа, и попытаться выполнить числовую операцию с полученным значением. Согласно стандарту языка, вы получите неопределённое поведение, то есть, что угодно имеет право произойти. Кроме того, определённые черты слабо типизированных языков есть и в C++: операция сложения между различными арифметическими типами вызывает неявную конверсию операндов к наиболее общему типу. Язык наподобие C# мог бы казаться ещё более статически типизированным (в нём нету аналога reinterpret_cast, и тип проверяется всегда), но зато в нём есть ключевое слово dynamic, близкое по идеологии к eval. Из особенностей динамических языков, перечисленных в Википедии, C# обладает в той или иной степени всеми, за исключением, пожалуй, макросов. Где-то посередине по богатству системы типов стоит SQL: в нём есть набор примитивных типов, но нет возможности расширить систему типов, применимо к нуждам пользователя. Итак, под динамической системой типов обычно понимается система типов, в которой меньше базовых типов, и/или некоторые операции могут применяться к разным типам с возможностью получения ошибки времени выполнения либо малоосмысленного результата. Большинство языков обладают этим свойством в той или иной мере. Литература для дальнейшего чтения: [1], [2], [3], [4]. Преимущества «более динамических» языков: Лучшая выразительность в некоторых сценариях, например, там где программист хочет сказать «я уверен, что этот объект умеет выполнить эту операцию» или «объекты, не умеющие делать это, игнорируются». Для сильнее типизированных языков такая семантика достигается использованием приведения типов или другой разновидностью проверки времени выполнения. Например, в Objective C, насколько я знаю, можно отправить произвольное сообщение произвольному объекту (разновидность duck typing); в C# для этого нужно, чтобы объект поддерживал соответствующий интерфейс, или использование dynamic, то есть откладывание проверки до момента выполнения. Неявная, «плавающая» семантика объектов по сравнению с явно заданными, продуманными и жёстко прописанными типами позволяет легко менять смысл классов и операций при разработке. (Не могу придумать пример для Objective C.) Это увеличивает скорость прототипирования, хотя, возможно, и негативно скажется на качестве кода при длительной разработке проекта. Отсюда распространённое мнение о том, что «динамические» языки лучше подходят для построения proof of concept, а статические — для имплементации. Более удобное общение с популярными нетипизированными или слаботипизированными структурами, такими как XML- и JSON-документы. Ещё один часто забываемый пример динамического свойства языков: виртуальные функции. Преимуществом более динамических языков является более простая диспетчеризация вызовов функций: если у меня определены f(Base, Base), f(Base, Derived), f(Derived, Base) f(Derived, Derived), язык с развитой динамической типизацией сможет автоматически выбрать нужную перегрузку при вызове метода, в то время как другие языки вынуждены пользоваться паттерном наподобие Visitor. Частным случаем диспетчеризации по одному, нулевому аргументу является вызов виртуальной функции. Некоторые языки умеют управлять кодом во время выполнения. Например, в Javascript'е можно переопределить любой метод через прототип, в Objective C можно подменить класс, используя категории (если я не ошибаюсь), в Питоне можно модифицировать поведение методов при помощи аннотаций. Достижение подобных эффектов в не приспособленных для этого языках сопряжено со значительным объёмом кода и нередко требует от пользователей неестественного, неудобного или даже опасного синтаксиса. (Например, оборачивать каждое объявление поля в макрос.) Суммируя: для понятий, встроенных в систему типов, динамически типизированные языки проигрывают статически типизированным в выразительной силе (пример: в статическом языке легко запретить складывать длину с весом, а в динамическом — нет). Для понятий, не описываемых системой типов, динамические языки выразительнее (пример всем объектам с полем возраст увеличить этот самый возраст на 1: в статических языках требует интерфейса, одинакового имени свойства и одинакового типа свойства). Многие преимущества, которые традиционно считаются преимуществами динамических языков, на самом деле лишь следствия хорошо организованной среды выполнения (рантайма), и в частности не требуют интерпретации или виртуальной машины. Например, интроспекция/рефлексия есть не вопрос динамичности, а лишь вопрос доступности метаданных во время выполнения, и вполне статичная с точки зрения системы типов Java её реализует. Замыкания, которые в Javascript'е есть чисто динамическая особенность, реализуются статически с полным контролем типизации в Objective C (под именем блоков) и C#. Английская Википедия считает, что динамические черты ... can be emulated in nearly any language of sufficient complexity, but dynamic languages provide direct tools to make use of them. (могут быть эмулированы в любом достаточно сложном языке, но динамические языки предоставляют прямой метод ими воспользоваться) — но я позволю себе частично не согласиться. То, как динамические свойства языка реализованы, может быть сделано при подходящей реализации и в любом другом языке, так что слово «эмуляция» здесь неприменимо: если статический язык «эмулирует» рефлексию при помощи метаданных, что же делает динамический? Итак, мы видим, что граница между статическими и динамическими языками довольно сильно размыта.

Ответ 2



Преимущества Минимум дополнительных строк: переменные надо либо просто объявить без указания типа (JavaScript), либо вообще объявлять не нужно (Бейсик) или не обязательно (PHP). Соответственно, упрощается написание простых программ Повышается гибкость языка. Например, только динамический язык может иметь функцию eval(), вычисляющую значение произвольного выражения. Ускоряет работу компилятора — а значит, производственный цикл «написать-проверить». Автоматически даёт языку элементы метапрограммирования и интроспекции. Упрощается работа прикладного программиста с СУБД, которые принципиально возвращают информацию в «динамически типизированном» виде. Поэтому динамические языки ценны, например, для программирования веб-служб. Иногда требуется работать с данными переменного типа. Например, функция поиска подстроки возвращает позицию найденного символа (число) или маркер «не найдено». В PHP этот маркер — булевское false. В статических языках это особая константа (0 в Паскале, std::string::npos в C++). Динамическая типизация Википедия

Разновидности реализаций языка Python

#python #дизайн_языка


В любой книге, где бы я не читал, говорится, что есть различные реализации языка.
  Стандартный это, как я знаю, CPython, а есть еще и другие (JPython, IronPython). 

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


Ответы

Ответ 1



Разницы в синтаксисе нет — каждый интерпретатор должен поддерживать грамматику Питона, чтобы иметь право называться таковым. Разница между интерпретаторами есть в скорости выполнения кода, доступности модулей: к примеру, не все модули стандартной библиотеки, что имплементированы в CPython, доступны в других реализациях, или имеют другую имплементацию. Модули, которые имплементируют часть функционала в С ("C extensions"), чаще всего не доступны ни с одним другим интерпретатором, кроме CPython'а (к примеру, сторонних реализаций numpy/scipy нигде, кроме как для PyPy, нет, да и там она добавлена практически вчера и в бете, ЕМНИП), мостиках в другие языки. К примеру, Jython умеет напрямую импортировать Java-классы - такой код кинет ImportError в других интерпретаторах: from java.util import Date from java.lang import System d = Date() System.out.println(d) Точно так же IronPython умеет работать с CLR и .NЕТ: from System import DateTime, String d = DateTime.now print String.Format("{0}", d)

Ответ 2



Язык задан описанием синтаксиса и грамматики и он, в общем случае, абстрактен. Реализация языка позволяет переводить код на языке в код, понятный процессору, чтобы он его мог выполнить. Реализации языка не отличаются в плане синтаксиса, но могут отличаться в плане семантики конструкций. Посмотрите, например, известное выражение i++ + ++i. Каждый интерпретатор понимает один и тот же код на Питоне, но переводит его в машинные коды немного по разному.

среда, 27 ноября 2019 г.

Зачем нужен static class?

#java #дизайн_языка


Статические переменные нужны для доступа к ним, без создания экземпляра класса. А
вот зачем нужен static класс?
    


Ответы

Ответ 1



Статическим классом в java может быть только вложенный класс. Если класс отмечен как static, то он ведет себя, как обычный класс. например, есть класс А, вложенный статический класс B и вложенный (нестатический) класс С: public class A{ ... static public class B{ } public class C{ } } и мы хотим создать экземпляры этих классов во "внешнем" коде public class Test{ public static void main(String[] args) { A a = new A(); // обычный класс A.B b = new A.B(); // статический вложенный класс A.C c = a.new C(); // вложенный класс, связан с экземпляром А // A.C c = new A.C(); // синтаксическая ошибка (не скомпилится) } } или внутри статических методов класса А public class A{ ... static public class B{ } public class C{ } public static void main(String[] args) { A a = new A(); // обычный класс A.B b = new A.B(); // статический вложенный класс A.C c = a.new C(); // вложенный класс, связан с экземпляром А // A.C c = new A.C(); // синтаксическая ошибка (не скомпилится) } public static void test() { A a = new A(); // обычный класс A.B b = new A.B(); // статический вложенный класс A.C c = a.new C(); // вложенный класс, связан с экземпляром А // A.C c = new A.C(); // синтаксическая ошибка (не скомпилится) } } На мой взгляд использование статического класса может быть уместно, как небольшой класс, который по смыслу тесно связан с "основным" внешним классом. Например: public class Tree{ static public class Node{ } } В этой ситуации так же можно вынести вложенный класс в обычный и переместить оба класса в отдельный package. Единственным отличием вложенного статического класса от обычного, которое мне видится, - это более снисходительное отношение к видимости методов и полей между вложенным классом и его внешним классом. Например: public class A { private void privateMethod(){ B b = new B(); b.privateMethod(); // есть доступ к приватным методам/полям } static public class B { private void privateMethod(){ A a = new A(); a.privateMethod(); // есть доступ к приватным методам/полям } } } Ссылка на документацию: https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html

Ответ 2



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

Ответ 3



"Статическим классом в java может быть только вложенный класс" - есть вложенные(static) и внутрение, разница как и в обычном плане, static имеет доступ только к статичным полям

пятница, 14 июня 2019 г.

Имплементация функционального интерфейса

Понимание интерфейса, как конструкции языка, до некоторых пор было мною вроде бы как усвоено. Столкнувшись с лямбда-выражениями это понимание рискует быть пересмотренным. Что я имею ввиду?
Интерфейс позволяет "объединить" в некотором смысле относительно разные классы и работать с ними под определенным углом (интерфейсом). Так или иначе, чтобы им (интерфейсом) воспользоваться, его необходимо имплементировать.
Функциональные интерфейсы, которые определяют один метод, ведут себя иначе? Их не нужно имплементировать, достаточно создать ссылку на них и присвоить этим ссылкам лямбда-выражение?
Мне интересен сам масштаб возможностей, который здесь открывается. Выходит просто ссылка на функциональный интерфейс внутри любого класса способна манипулировать объектами любого класса?


Ответ

Просто как пример, что же такое лямбда в Java
Лямбда
new AccidentsRequest(result -> {if (!result.has("error")) parseJSON(result);}, true);
ТРАХ-ТИБИДОХ, АХАЛАЙ-МАХАЛАЙ!!! И лямбда превращается... В анонимный класс!
new AccidentsRequest(new AsyncTaskCompleteListener() { @Override public void onTaskComplete(JSONObject result) throws JSONException { if (!result.has("error")) Content.this.parseJSON(result); } }, true);
Фактически лямбды в Java - это такой синтаксический сахар для анонимных классов с одним методом.