Яку функціональність дозволяє динамічне введення тексту? [зачинено]


91

Я вже кілька днів використовую python, і я думаю, що розумію різницю між динамічним та статичним типізацією. Я не розумію, за яких обставин було б кращим. Він гнучкий і читабельний, але за рахунок більшої кількості перевірок виконання та додаткових необхідних тестових одиниць.

Окрім нефункціональних критеріїв, таких як гнучкість та читаність, які причини існують для вибору динамічного введення тексту? Що я можу зробити з динамічним введенням тексту, яке інакше неможливо? Який конкретний приклад коду ви можете уявити, що ілюструє конкретну перевагу динамічного набору тексту?


5
Теоретично немає нічого, чого ви не можете зробити ні до тих пір, поки мови є Turing Complete . Більш цікаве питання для мене - що легко чи природно в одному проти іншого. У Python я регулярно займаюсь речами, які я навіть не вважав би на C ++, хоча я знаю, що він здатний.
Марк Рансом

28
Як пише Кріс Сміт у своєму чудовому нарисі Що потрібно знати, перш ніж обговорювати системи типів : "Проблема в цьому випадку полягає в тому, що більшість програмістів мають обмежений досвід і не перепробували багато мов. Для контексту тут шість або сім не рахується як "багато". Дві цікаві наслідки цього: (1) Багато програмістів користуються дуже поганими статично типовими мовами. (2) Багато програмістів дуже погано використовують динамічно набрані мови ".
Даніель Приден

3
@suslik: Якщо мовні примітиви мають безглузді типи, то, звичайно, ви можете робити безглузді речі з типами. Це не має нічого спільного з різницею між статичним та динамічним введенням тексту.
Джон Перді

10
@CzarekTomczak: Це особливість деяких динамічно набраних мов, так. Але можлива зміна статично типової мови під час виконання. Наприклад, Visual Studio дозволяє переписати код C #, коли ви знаходитесь на точці розриву на відладці, і навіть перемотати покажчик інструкцій для повторного запуску коду з новими змінами. Як я цитував Кріса Сміта в своєму іншому коментарі: "Багато програмістів користуються дуже поганими статично типовими мовами" - не судіть усіх статично введених мов за тими, які ви знаєте.
Даніель Приден

11
@WarrenP: Ви стверджуєте, що "системи динамічного типу зменшують кількість зайвої сукупності, яку я маю набрати", - але ви порівнюєте Python з C ++. Це не справедливе порівняння: звичайно, C ++ є більш багатослівним, ніж Python, але це не через різницю в їхніх типах систем, це через різницю в їхніх граматиках. Якщо ви просто хочете зменшити кількість символів у джерелі програми, вивчіть J або APL: Я гарантую, що вони будуть коротшими. Більш справедливим порівнянням було б порівняння Python з Haskell. (Для запису: я люблю Python і вважаю за краще його над C ++, але мені подобається Хаскелл ще більше.)
Даніель Приден

Відповіді:


50

Оскільки ви попросили конкретного прикладу, я наведу вам один.

Масивна ORM Роб Конери - 400 рядків коду. Це так мало, тому що Роб вміє відображати таблиці SQL і надавати результати об’єктів, не вимагаючи багато статичних типів для відображення таблиць SQL. Це досягається за допомогою dynamicтипу даних у C #. Веб-сторінка Роба детально описує цей процес, але видається зрозумілим, що в даному конкретному випадку використання динамічного набору тексту значною мірою відповідає за стислість коду.

Порівняйте з Dapper Сема Шафрона , який використовує статичні типи; SQLMapperв поодинці клас 3000 рядків коду.

Зверніть увагу, що застосовуються звичайні відмови від відповідальності, і пробіг може змінюватися; Дапер має інші цілі, ніж Massive. Я просто зазначу це як приклад того, що ви можете зробити в 400 рядках коду, що, ймовірно, не було б можливим без динамічного набору тексту.


Динамічне введення тексту дозволяє відкласти рішення типу на час виконання. Це все.

Незалежно від того, чи використовуєте ви мову з динамічним набором або статично введений, вибір вашого типу все одно повинен бути розумним. Ви не збираєтесь додавати два рядки разом і не очікуєте числової відповіді, якщо рядки не містять числових даних, а якщо їх немає, ви отримаєте несподівані результати. Статично введена мова не дозволить вам це зробити в першу чергу.

Прихильники мов статичного типу зазначають, що компілятор може зробити значну кількість "перевірки правильності" вашого коду під час компіляції, перш ніж виконати один рядок. Це хороша річ ™.

C # має dynamicключове слово, яке дозволяє відкласти рішення типу на час виконання, не втрачаючи переваг безпеки статичного типу в решті коду. Виведення типу ( var) усуває значну частину болю при написанні мовою статичного типу, усуваючи необхідність завжди чітко оголошувати типи.


Динамічні мови, схоже, віддають перевагу більш інтерактивному, негайному підходу до програмування. Ніхто не очікує, що вам доведеться написати клас і пройти цикл компіляції, щоб набрати трохи коду Lisp і переглянути його виконання. І все-таки саме те, що я очікую зробити в C #.


22
Якби я додав два числові рядки разом, я все одно не очікував би числового результату.
пдр

22
@Robert Я згоден з більшістю вашої відповіді. Однак зауважте, що існують мови статичного типу з інтерактивними петлями для читання-друку, такі як Scala та Haskell. Можливо, C # просто не є особливо інтерактивною мовою.
Андрес Ф.

14
Треба навчитися мені Haskell.
Роберт Харві

7
@RobertHarvey: Ви можете бути здивовані / вражені F #, якщо ви ще не пробували цього. Ви отримуєте всю безпеку типу (час компіляції), яку ви зазвичай отримуєте мовою .NET, за винятком того, що вам рідко доводиться декларувати будь-які типи. Виведення типу в F # виходить за рамки доступних / працює в C #. Також: подібно до того, що вказують Андрес і Даніель, F # interactive є частиною Visual Studio ...
Стівен Еверс

8
"Ви не збираєтесь додавати два рядки разом і не очікуєте числової відповіді, якщо рядки не містять числових даних, а якщо їх немає, ви отримаєте несподівані результати" Вибачте, це не має нічого спільного з динамічним проти статичним введенням тексту , це сильний і слабкий набір тексту.
vartec

26

Фрази на зразок "статичне введення" та "динамічне введення тексту" дуже багато, і люди, як правило, використовують тонко різні визначення, тож почнемо з роз'яснення, що ми маємо на увазі.

Розглянемо мову, яка має статичні типи, які перевіряються під час компіляції. Але скажіть, що помилка типу породжує лише нефатальне попередження, а під час виконання все набирається качка. Ці статичні типи призначені лише для зручності програміста і не впливають на кодеген. Це ілюструє, що статичне введення само по собі не накладає жодних обмежень і не є взаємовиключним при динамічному наборі тексту. (Objective-C багато в чому подібний.)

Але більшість систем статичного типу не ведуть себе таким чином. Є дві загальні властивості систем статичного типу, які можуть накладати обмеження:

Компілятор може відхилити програму, яка містить помилку статичного типу.

Це обмеження, оскільки багато безпечних програм типу обов'язково містять помилку статичного типу.

Наприклад, у мене є сценарій Python, який потрібно запускати як Python 2, так і Python 3. Деякі функції змінили свої типи параметрів між Python 2 і 3, тому у мене є такий код:

if sys.version_info[0] == 2:
    wfile.write(txt)
else:
    wfile.write(bytes(txt, 'utf-8'))

Перевірка статичного типу Python 2 відхилить код Python 3 (і навпаки), хоча він ніколи не буде виконуватися. Моя безпечна програма містить помилку статичного типу.

В якості іншого прикладу розглянемо програму Mac, яка хоче працювати на OS X 10.6, але скористатися новими можливостями в 10.7. Методи 10.7 можуть або не існують під час виконання, і саме на мене, програміста, їх можна виявити. Перевірка статичного типу змушена або відхилити мою програму для забезпечення безпеки типу, або прийняти програму разом з можливістю створення помилки типу (функція відсутня) під час виконання.

Статична перевірка типу передбачає, що середовище виконання адекватно описано інформацією про час компіляції. Але прогнозувати майбутнє небезпечно!

Ось ще одне обмеження:

Компілятор може генерувати код, який передбачає, що тип виконання - це статичний тип.

Якщо припустити, що статичні типи є "правильними", то існує багато можливостей для оптимізації, але ці оптимізації можуть бути обмежуючими. Хороший приклад - проксі-об'єкти, наприклад, видалення. Скажімо, ви хочете мати локальний проксі-об'єкт, який пересилає виклики методу до реального об'єкта в іншому процесі. Було б добре, якби проксі був загальним (таким чином він може маскуватися як будь-який об’єкт) і прозорим (так що існуючий код не повинен знати, що він говорить з проксі). Але для цього компілятор не може генерувати код, який передбачає, що статичні типи є правильними, наприклад, за допомогою статично вбудованих викликів методу, оскільки це не вдасться, якщо об'єкт насправді є проксі.

Приклади такого видалення в дії включають NSXPCConnection ObjC або TransparentProxy C # (реалізація яких вимагала декількох песимізацій під час виконання - дивіться тут для обговорення).

Коли кодеген не залежить від статичних типів, і у вас є такі засоби, як пересилання повідомлень, ви можете робити багато цікавих речей з проксі-об'єктами, налагодження тощо.

Отже, це вибірки деяких речей, які ви можете зробити, якщо від вас не вимагається перевірити тип перевірки. Обмеження не накладаються статичними типами, а примусовою перевіркою статичного типу.


2
"Програма перевірки статичного типу Python 2 відхилить код Python 3 (і навпаки), навіть якщо він ніколи не буде виконуватися. Моя безпечна програма типу містить помилку статичного типу." Звучить, що вам справді потрібно, є якийсь "статичний if", де компілятор / інтерпретатор навіть не бачить код, якщо умова хибна.
Девід Стоун

@davidstone, що існує в c ++
Milind R

A Python 2 static type checker would reject the Python 3 code (and vice versa), even though it would never be executed. My type safe program contains a static type error. На будь-якій розумній статичній мові ви можете це зробити за допомогою оператора IFDEFтипу препроцесора, зберігаючи безпеку типу в обох випадках.
Мейсон Уілер

1
@MasonWheeler, davidstone Ні, хитрості препроцесора та static_if є надто статичними. У своєму прикладі я використовував Python2 та Python3, але це могло бути легко, як AmazingModule2.0 та AmazingModule3.0, де деякий інтерфейс змінювався між версіями. Найперше, що вам відомо про інтерфейс, є час імпорту модуля, який обов'язково виконується під час виконання (принаймні, якщо у вас є бажання підтримувати динамічні зв'язки).
ridiculous_fish

18

Змінні, набрані качками, - це перше, про що думають, але в більшості випадків ви можете отримати однакові переваги за допомогою статичного виводу.

Але введення качок у динамічно створені колекції важко досягти будь-яким іншим способом:

>>> d = JSON.parse(foo)
>>> d['bar'][3]
12
>>> d['baz']['qux']
'quux'

Отже, який тип JSON.parseповертається? Словник масивів цілих чисел-або-словників-рядків? Ні, навіть це не досить загально.

JSON.parseмає повернути якесь "значення значення", яке може бути нульовим, bool, float, string, масивом будь-якого з цих типів рекурсивно або словником з рядка до будь-якого з цих типів рекурсивно. Основні переваги динамічного набору тексту полягають у наявності таких варіантів.

Поки що це є перевагою динамічних типів , а не динамічно набраних мов. Гідна статична мова може ідеально імітувати будь-який подібний тип. (І навіть "погані" мови часто можуть імітувати їх, порушуючи безпеку типу під кришкою та / або вимагаючи незграбного синтаксису доступу.)

Перевага мов, що динамічно набираються, полягає в тому, що такі типи не можуть бути встановлені системами статичного виводу. Тип потрібно чітко записати. Але у багатьох таких випадках - включаючи цей раз - код для опису типу є настільки ж складним, як і код для розбору / побудови об'єктів, не описуючи тип, так що все одно не обов'язково є перевагою.


21
Ваш приклад розбору JSON легко статично обробляється типом алгебраїчних даних.

2
Гаразд, моя відповідь була недостатньо чіткою; Дякую. Це JSValue - це чітке визначення динамічного типу, саме те, про що я говорив. Це корисні динамічні типи, а не мови, які потребують динамічного введення тексту. Однак все ще актуально, що динамічні типи не можуть бути автоматично генеровані жодною реальною системою виводу типу, в той час як більшість поширених прикладів людей є тривіально невимірними. Сподіваюся, нова версія пояснює це краще.
abarnert

4
Типи алгебраїчних даних @MattFenwick в значній мірі обмежені функціональними мовами (на практиці). Що щодо таких мов, як Java та c #?
spirc

4
ADT існують у C / C ++ як позначені об'єднання. Це не властиво лише функціональним мовам.
Кларк Гебель

2
@spirc Ви можете імітувати ADT в класичній мові OO, використовуючи кілька класів, які походять від загального інтерфейсу, викликів під час виконання getClass () або GetType () та перевірки рівності. Або ви можете скористатися подвійною відправленням, але я думаю, що це окупається більше в C ++. Таким чином, у вас може бути інтерфейс JSObject і JSString, JSNumber, JSHash та JSArray. Тоді вам знадобиться якийсь код, щоб перетворити цю "нетипізовану" структуру даних у структуру даних "набрав додаток". Але ви, мабуть, хотіли б зробити це і динамічно набраною мовою.
Даніель Янковський

12

Оскільки кожна дистанційно практична система статичного типу сильно обмежена в порівнянні з мовою програмування, якою вона стосується, вона не може висловити всіх інваріантів, коди яких можна перевірити під час виконання. Щоб не обійти гарантії, яку намагається надати система типів, вона, отже, вирішує бути консервативною та забороняти використовувати випадки, які могли б пройти ці перевірки, але не можуть бути доведені (у системі типів).

Я зроблю приклад. Припустимо, ви реалізуєте просту модель даних для опису об'єктів даних, їх колекцій тощо, яка статично набрана в тому сенсі, що, якщо модель говорить, що атрибут xоб'єкта типу Foo має ціле число, він завжди повинен містити ціле число. Оскільки це конструкція виконання, ви не можете вводити його статично. Припустимо, ви зберігаєте дані, описані у файлах YAML. Ви створюєте хеш-карту (щоб потім бути переданою бібліотеці YAML), отримуєте xатрибут, зберігаєте його на карті, отримуєте інший атрибут, який просто так буває рядок, ... затримаєте секунду? Який тип the_map[some_key]зараз? Добре стріляйте, ми знаємо, що some_keyце є, 'x'а результат, таким чином, повинен бути цілим числом, але система типу навіть не може почати міркувати про це.

Деякі системи досліджуваного типу можуть працювати для цього конкретного прикладу, але вони надзвичайно складні (як для авторів-компіляторів, що реалізують, так і для програміста, що міркують), особливо для чогось такого "простого" (я маю на увазі, я просто пояснив це в одному абзац).

Звичайно, сьогоднішнє рішення - боксувати все, а потім робити кастинг (або мати купу перекритих методів, більшість з яких піднімають винятки "не реалізовані"). Але це не статично набрано, це хак навколо системи типів, щоб робити перевірки типу під час виконання.


Загальні типи не мають вимоги до боксу.
Роберт Харві

@RobertHarvey Так. Я не говорив про бокс на Java C #, я говорив про "загортання в якийсь клас обгортки, єдиною метою якого є значення T у підтипі U". Параметричний поліморфізм (те, що ви називаєте, загальне введення тексту), однак, не стосується мого прикладу. Це абстракція в часі компіляції над конкретними типами, але нам потрібен механізм набору тексту для виконання.

Можливо, варто зазначити, що система типу Scala є Тюрінгом завершеною. Тож системи типу можуть бути менш тривіальними, ніж ви малюєте.
Андреа

@Andrea Я навмисно не зводив свій опис до твердості. Коли-небудь запрограмований у витримці брезенту? Або спробували закодувати ці речі за типами? У якийсь момент це стає занадто складним, щоб бути можливим.

@delnan Я згоден Я тільки вказував, що системи типу можуть робити досить складні речі. У мене склалося враження, що ваша відповідь означає, що система типу може робити лише тривіальну перевірку, але при другому прочитанні ви нічого подібного не написали!
Андреа

7

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

Короткий приклад у Haskell:

data Data = DString String | DInt Int | DDouble Double

-- defining a '+' operator here, with explicit promotion behavior
DString a + DString b = DString (a ++ b)
DString a + DInt b = DString (a ++ show b)
DString a + DDouble b = DString (a ++ show b)
DInt a + DString b = DString (show a ++ b)
DInt a + DInt b = DInt (a + b)
DInt a + DDouble b = DDouble (fromIntegral a + b)
DDouble a + DString b = DString (show a ++ b)
DDouble a + DInt b = DDouble (a + fromIntegral b)
DDouble a + DDouble b = DDouble (a + b)

У достатній кількості випадків ви можете реалізувати будь-яку задану систему динамічного типу.

І навпаки, ви також можете перевести будь-яку статично набрану програму в еквівалентну динамічну програму. Звичайно, ви втратите всі запевнення часу компіляції у правильності, які надає статично набрана мова.

Редагувати: Я хотів зберегти це просто, але ось детальніше про модель об'єкта

Функція приймає список даних як аргументи і виконує обчислення з побічними ефектами в ImplMonad і повертає дані.

type Function = [Data] -> ImplMonad Data

DMember це або значення члена, або функція.

data DMember = DMemValue Data | DMemFunction Function

Розширити, Dataщоб включити Об'єкти та функції. Об'єктами є списки названих членів.

data Data = .... | DObject [(String, DMember)] | DFunction Function

Цих статичних типів достатньо для реалізації кожної динамічно типізованої об'єктної системи, з якою я знайомий.


Це зовсім не те саме, оскільки ви не можете додавати нові типи, не переглядаючи визначення Data.
Джед

5
Ви змішуєте в своєму прикладі поняття динамічного введення зі слабким набором тексту. Динамічне введення тексту - це робота з невідомими типами, а не визначення списку дозволених типів та операцій перевантаження між ними.
hcalves

2
@Jed Після того, як ви реалізували об'єктну модель, фундаментальні типи та примітивні операції, ніяких інших основ не потрібно. Ви можете легко та автоматично перекладати програми на оригінальній динамічній мові на цей діалект.
NovaDenizen

2
@hcalves Оскільки ви посилаєтесь на перевантаження в моєму коді Haskell, я підозрюю, що ви не маєте правильного уявлення про його семантику. Там я визначив нового +оператора, який поєднує два Dataзначення в інше Dataзначення. Dataпредставляє стандартні значення в системі динамічного типу.
NovaDenizen

1
@Jed: Більшість динамічних мов мають невеликий набір "примітивних" типів та індуктивний спосіб введення нових значень (структури даних, такі як списки). Наприклад, схема набуває досить далеко з трохи більше атомів, пар і векторів. Ви повинні мати можливість реалізувати їх так само, як і решта заданого динамічного типу.
Тихон Єлвіс

3

Мембрани :

Мембрана - це обгортка навколо всього графіка об'єкта, на відміну від обгортки лише для одного об’єкта. Зазвичай творець мембрани починає загортати в мембрану лише один предмет. Ключова ідея полягає в тому, що будь-яка посилання на об'єкт, що перетинає мембрану, сама транзитивно загортається в ту ж мембрану.

введіть тут опис зображення

Кожен тип обгортається типом, який має той самий інтерфейс, але який перехоплює повідомлення, перегортає та розкручує значення, коли вони перетинають мембрану. Який тип функції обтікання улюбленою статично введеною мовою? Можливо, у Haskell є тип для цих функцій, але більшість статично набраних мов не використовують або вони в кінцевому підсумку використовують Object → Object, фактично відмовляючись від своєї відповідальності як перевіряючих типів.


4
Так, Haskell дійсно може це досягти, використовуючи екзистенційні типи. Якщо у вас є якийсь клас типу Foo, ви можете зробити обгортку навколо будь-якого типу, інстанціюючи цей інтерфейс. class Foo a where ... data Wrapper = forall a. Foo a => Wrapper a
Джейк МакАртюр

@JakeMcArthur, Дякую за пояснення. Це ще одна причина для мене сісти і вивчити Хаскелл.
Майк Самуель

2
Ваша мембрана - це «інтерфейс», а типи об’єктів «екзистенційно набрані» - тобто ми знаємо, що вони існують під інтерфейсом, але це все, що ми знаємо. Екзистенційні типи абстрагування даних відомі з 80-х років. Хороший відгук - cs.cmu.edu/~rwh/plbook/book.pdf розділ 21.1
Дон Стюарт

@DonStewart. Чи є тоді проксі-класи Java механізмом екзистенційного типу? Одне місце, де мембрани стають складними, - це мови з системами номінального типу, які мають назви конкретних типів, видимих ​​поза визначенням цього типу. Наприклад, його не можна обернути, Stringоскільки це конкретний тип у Java. У Smalltalk цієї проблеми немає, оскільки вона не намагається ввести текст #doesNotUnderstand.
Майк Самуель

1

Як хтось згадував, теоретично не можна багато чого зробити з динамічним набором тексту, якого ти не міг би зробити зі статичним набором тексту, якщо ти самостійно реалізуєш певні механізми. Більшість мов надають механізми релаксації типів для підтримки гнучкості типу, таких як пусті покажчики, і тип кореневого об’єкта або порожній інтерфейс.

Питання в тому, чому динамічне введення тексту підходить і підходить у певних ситуаціях та проблемах.

Спочатку давайте визначимось

Суб'єкт - мені потрібно загальне поняття про якусь сутність у коді. Це може бути все, від примітивного числа до складних даних.

Поведінка - скажімо, у нашої сутності є певний стан і набір методів, які дозволяють навколишньому світу навчити суб'єкта певних реакцій. Дозволяє називати стан + інтерфейс цього об'єкта його поведінкою. Одне об'єднання може мати більше, ніж одна поведінка, певним чином поєднана засобами мови, що надаються.

Визначення сутностей та їх поведінки - кожна мова містить певні засоби абстракцій, які допомагають визначити поведінку (набір методів + внутрішній стан) певних сутностей у програмі. Ви можете призначити ім'я такої поведінки і сказати, що всі випадки такої поведінки мають певний тип .

Це, мабуть, щось, що не таке незнайоме. І як ви сказали, ви зрозуміли різницю, але все ж. Напевно, не повне і найточніше пояснення, але я сподіваюся, що веселощі достатньо, щоб принести певну цінність :)

Статичне введення тексту - поведінка всіх об'єктів у вашій програмі вивчається за час компіляції, перш ніж почати запуск коду. Це означає, що якщо ви хочете, наприклад, у вашої сутності типу Person мати поведінку (поводитись, як) Magician, то вам доведеться визначити сутність MagicianPerson і надати їй поведінку фокусника на зразок castMagic (). Якщо ви в своєму коді, помилково скажіть звичайному компілятору Person.throwMagic ()"Error >>> hell, this Person has no this behavior, dunno throwing magics, no run!".

Динамічне введення тексту - в умовах динамічного набору тексту поведінка сутностей не перевіряється, поки ви дійсно не спробуєте зробити щось із певною сутністю. Запуск коду Ruby, який запитує Person.throwMagic (), не буде спійманий, поки ваш код дійсно не надійде. Це звучить неприємно, чи не так? Але це також звучить викривально. На основі цієї властивості ви можете робити цікаві речі. Скажімо, скажімо, ви розробляєте гру, де все може звернутися до Magician, і ви насправді не знаєте, хто це буде, поки ви не дійдете до певного коду. І тоді приходить Жаба, і ти кажешHeyYouConcreteInstanceOfFrog.include Magicі відтоді ця Жаба стає однією конкретною Жабою, яка має Магічні сили. Інші Жаби, ще ні. Розумієте, в мовах статичного введення вам доведеться визначити це відношення за допомогою якогось стандартного середнього значення комбінації поведінки (наприклад, реалізація інтерфейсу). У динамічній мові набору тексту ви можете це робити під час виконання і нікому не буде байдуже.

Більшість мов динамічного набору тексту мають механізми, що забезпечують загальну поведінку, яка вловлює будь-яке повідомлення, яке передається їх інтерфейсу. Наприклад, Ruby method_missingі PHP, __callякщо я добре пам'ятаю. Це означає, що ви можете робити будь-які цікаві речі під час виконання програми та приймати рішення типу, виходячи з поточного стану програми. Це дає інструменти для моделювання проблеми, які є набагато більш гнучкими, ніж, скажімо, консервативна статична мова програмування, як Java.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.