Як писати корисні програми Java без використання змінних змінних


12

Я читав статтю про функціональне програмування, де автор пише

(take 25 (squares-of (integers)))

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

Чи можна цього досягти на Java? Припустимо, що ви повинні надрукувати квадрати перших 15 цілих чисел, чи можете ви написати цикл для або під час без використання змінних?

Модне повідомлення

Це питання не є змаганням з кодового гольфу. Ми шукаємо відповіді, які пояснюють поняття, що стосуються (в ідеалі, не повторюючи попередніх відповідей), а не лише ще один фрагмент коду.


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

12
@Blrfl: Аргумент "за кадром" вбиває всі дискусії на основі мови, оскільки кожен фрагмент коду в кінцевому підсумку переводиться на машинний код x86. Код x86 не об'єктно-орієнтований, не процедурний, не функціональний, нічого, але ці категорії є цінними тегами для мов програмування. Подивіться на мову, а не на реалізацію.
титон

10
@thiton Не погоджується. Що говорить Blrfl, це те, що ці функції, ймовірно, використовують змінні, написані однією мовою програмування . Тут не потрібно йти низько. Фрагмент використовує лише функції бібліотеки. Ви можете легко написати той самий код на Java, дивіться: squaresOf(integers()).take(25)(написання цих функцій залишається вправою для читача; складність полягає в нескінченному наборі для integers(), але це проблема для Java через її прагнення до оцінки, нічого спільного з змінні)
Андрес Ф.

6
Ця цитата заплутана і вводить в оману, там немає ніякої магії, лише синтаксичний цукор .
янніс

2
@thiton Я пропоную вам дізнатися більше про мови FP, але, тим не менш, фрагмент коду не підтримує (або відкидає) твердження про те, що "змінні" не потрібні (під яким я вважаю, ви маєте на увазі "змінні змінні", тому що інше вид поширений у ФП). У фрагменті просто показані функції бібліотеки, які могли бути реалізовані і на Java, що забороняє ледачі / нетерплячі проблеми, які тут не впадають у тему.
Андрес Ф.

Відповіді:


31

Чи можливо реалізувати такий приклад на Java без використання руйнівних оновлень? Так. Однак, як згадували @Thiton та сама стаття, це буде некрасиво (залежно від смаку). Один із способів - використання рекурсії; ось приклад Haskell, який робить щось подібне:

unfoldr      :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr f b  =
  case f b of
   Just (a,new_b) -> a : unfoldr f new_b
   Nothing        -> []  

Примітка 1) відсутність мутації, 2) використання рекурсії та 3) відсутність петель. Останній пункт дуже важливий - функціональні мови не потребують циклічних конструкцій, вбудованих у мову, оскільки рекурсія може використовуватися для більшості (усіх?) Випадків, коли петлі використовуються на Java. Ось відома серія статей, що показують, як неймовірно виразні дзвінки функцій можуть бути.


Я вважав статтю незадовільною і хотів би зробити кілька додаткових моментів:

Ця стаття є дуже бідним і заплутаним поясненням функціонального програмування та його переваг. Настійно рекомендую інші джерела для вивчення функціонального програмування.

Найбільш заплутаною частиною статті є те, що в ній не згадується, що у Java (і більшості інших основних мов) є два варіанти використання операторів призначення:

  1. прив’язування значення до імені: final int MAX_SIZE = 100;

  2. руйнівне оновлення: int a = 3; a += 1; a++;

Функціональне програмування ухиляється від другого, але охоплює перше (приклади: let-вираження, параметри функції, defineіони верхнього рівня ) . Це дуже важливий момент для розуміння, тому що в іншому випадку ця стаття просто здається дурною і може залишити вас цікаво, що це take, squares-ofі integersякщо не змінні?

Крім того, приклад є безглуздим. Він не показує реалізації take, squares-ofабо integers. Наскільки ми знаємо, вони реалізовані за допомогою змінних змінних. Як сказав @Martin, цей приклад можна тривіально написати на Java.

Ще раз рекомендую уникати цієї статті та інших, хто їй подобається, якщо ви дійсно хочете дізнатися про функціональне програмування. Здається, написано більше з метою шокувати і ображати, а не викладати поняття та основи. Натомість, чому б не перевірити один з моїх улюблених за весь час улюблених робіт Джона Х'юза. Х'юз намагається вирішити деякі ті ж питання, що й стаття, що охоплювала статтю (хоча Х'юз не говорить про одночасність / паралелізацію); ось тизер:

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

[...]

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


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

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

3
Поставте свою відповідь на питання в абсолютній вершині, якщо ви хочете, щоб це було більш прямим питанням, і, можливо, це питання залишатиметься відкритим тоді (з прямою відповіді, орієнтованої на питання)
Джиммі Хоффа

2
Вибачте за nitpick, але я не розумію, чому ви вибрали цей код haskell. Я читав ЛЯХ, і твій приклад важко для мене. Я також не бачу відношення до початкового питання. Чому ви не просто використали take 25 (map (^2) [1..])як приклад?
Даніель Каплан

2
@tieTYT гарне запитання - дякую, що вказали на це. Я використовував цей приклад тому, що він показує, як генерувати список чисел за допомогою рекурсії та уникаючи змінних змінних. Моя мета полягала в тому, щоб ОП побачив цей код і подумав, як зробити щось подібне на Java. Що стосується вашого фрагмента коду, що таке [1..]? Це класна функція, вбудована в Haskell, але не ілюструє поняття, що стоять за формуванням такого списку. Я впевнений, що екземпляри Enumкласу (якого вимагає цей синтаксис) також корисні, але їх ліно було знайти. Таким чином, unfoldr. :)

27

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

Отже, способом Java було б:

for (int i = 1; i <= 25; ++i) {
    System.out.println(i*i);
}

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


5
"Ненависть до змінних"? Ooookay ... Що ви читали про функціональне програмування? Які мови ви пробували? Які навчальні посібники?
Андрес Ф.

8
@AndresF: Більше двох років курсових робіт у Хаскеллі. Я не кажу, що FP - це погано. Однак у багатьох дискусіях FP-IP-IP (наприклад, пов'язана стаття) є тенденція засуджувати використання повторно присвоєних названих утворень (змінні AKA) та засуджувати без поважних причин чи даних. Необосноване осуд - це ненависть у моїй книзі. І ненависть створює дійсно поганий код.
титон

10
"Ненависть до змінних" є причинним надмірним спрощенням en.wikipedia.org/wiki/Fallacy_of_the_single_, оскільки для програмування без громадянства існує багато переваг, які навіть у Java можуть бути, хоча я згоден з вашою відповіддю, що вартість у Java буде зависокою за складністю програма і будучи неідіоматичною. Я б все одно не обійшов рукою, маючи думку про те, що програмування без громадянства - це добре, а стан - це погано, як емоційна реакція, а не аргументована, продумана позиція людей, що виникають через досвід.
Джиммі Хоффа

2
У відповідь на те, що говорить @JimmyHoffa, я посилаюсь на Джона Кармака на тему функціонального програмування в імперативних мовах (C ++ у його випадку) ( altdevblogaday.com/2012/04/26/functional-programming-in-c ).
Стівен Еверс

5
Нерозумне засудження - це не ненависть, а уникнення змінних станів не є необґрунтованим.
Майкл Шоу

21

Найпростіша, що я можу зробити з рекурсією, - це функція з одним параметром. Це не дуже Java-esque, але це працює:

public class squares
{
    public static void main(String[] args)
    {
        squares(15);
    }

    private static void squares(int x)
    {
        if (x>0)
        {
            System.out.println(x*x);
            squares(x-1);
        }
    }
}

3
+1 для спроби реально відповісти на питання на прикладі Java.
KChaloux

Я б схвалив це для презентації стилю гольф-коду (див. Повідомлення Мода ), але не можу змусити себе натискати стрілку вниз, оскільки цей код ідеально відповідає заявам, зробленим у моїй улюбленій відповіді : "1) відсутність мутації; 2) використання рекурсія і 3) відсутність петель "
гнат

3
@gnat: Ця відповідь була розміщена перед повідомленням Мода. Я не збирався чудовим стилем, я збирався простотою та задоволенням оригінального питання ОП; щоб показати , що це можна робити такі речі в Java.
FrustratedWithFormsDesigner

@FrustratedWithFormsDesigner впевнений; це не зупинить мене від DVing (оскільки ви, мабуть, зможете редагувати відповідність) - це надзвичайно ідеальне поєднання, яке зробило магію. Молодці, по-справжньому молодець, досить виховні - дякую
gnat

16

У вашому функціональному прикладі ми не бачимо, як реалізуються функції squares-ofта takeфункції. Я не експерт по Java, але я впевнений, що ми могли б написати ці функції, щоб увімкнути таке твердження ...

squares_of(integers).take(25);

що не так вже й відрізняється.


6
Nitpick: невірне squares-ofім’я в Java ( squares_ofвсе ж). Але в іншому випадку, хороший момент, який показує, що приклад статті поганий.

Я підозрюю, що стаття integerліниво генерує цілі числа, а takeфункція вибирає 25 squared-ofчисел із integer. Коротше кажучи, ви повинні мати integerфункцію ліниво створювати цілі числа до нескінченності.
OnesimusUnbound

Трохи божевільним називати щось на зразок (integer)функції - функція все- таки є те, що відображає аргумент на значення. Виявляється, (integer)це не функція, а лише цінність. Можна навіть зайти так далеко, щоб сказати, що integerце змінна, яка пов'язана з нескінченним потоком чисел.
Інго

6

На Java ви могли б зробити це (особливо частина нескінченного списку) за допомогою ітераторів. У наступному зразку коду число, що подається Takeконструктору, може бути довільно високим.

class Example {
    public static void main(String[] a) {
        Numbers test = new Take(25, new SquaresOf(new Integers()));
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Або за допомогою заводських методів:

class Example {
    public static void main(String[] a) {
        Numbers test = Numbers.integers().squares().take(23);
        while (test.hasNext())
            System.out.println(test.next());
    }
}

Де SquaresOf, Takeі IntegersпродовжитиNumbers

abstract class Numbers implements Iterator<Integer> {
    public static Numbers integers() {
        return new Integers();
    }

    public Numbers squares() {
        return new SquaresOf(this);
    }

    public Numbers take(int c) {
        return new Take(c, this);
    }
    public void remove() {}
}

1
Це показує перевагу парадигми ОО над функціональною; при правильному дизайні ОО ви можете імітувати функціональну парадигму, але не можете імітувати парадигму ОО в функціональному стилі.
m3th0dman

3
@ m3th0dman: При правильному дизайні ОО ви можете, можливо, напівсистематично імітувати FP, як і будь-яка мова, що має рядки, списки та / або словники, може наполовину імітувати OO. Еквівалентність Тьюрінга мов загального призначення означає, що, докладаючи достатньо зусиль, будь-яка мова може імітувати особливості будь-якої іншої.
cHao

Зауважте, що ітератори в стилі Java, як in, while (test.hasNext()) System.out.println(test.next())були б не FP; ітератори за своєю суттю змінюються.
cHao

1
@cHao Я навряд чи вірю, що справжнє капсулювання чи поліморфізм можна наслідувати; також Java (у цьому прикладі) не може по-справжньому імітувати функціональну мову через сувору прагнення до оцінки. Я також вважаю, що ітератори можуть бути записані рекурсивно.
m3th0dman

@ m3th0dman: Поліморфізм зовсім не важко буде імітувати; навіть C і мова монтажу можуть це зробити. Просто зробіть метод полем в об'єкті або дескрипторі / vtable класу. І інкапсуляція в сенсі приховування даних не є строго необхідною; половина мов там не надають цього, коли ваш об’єкт незмінний, не важливо, чи люди можуть бачити його кишки в будь-якому разі. все, що потрібно, - це обробка даних , які вищезазначені поля методів могли легко надати.
cHao

6

Коротка версія:

Для того, щоб стиль Java-надії надійно працював на Java, вам знадобиться (1) якась непорушна інфраструктура та (2) підтримка компілятора або рівня виконання для усунення хвостових викликів.

Ми можемо написати велику частину інфраструктури і можемо домовитись про те, щоб уникнути заповнення стека. Але поки кожен виклик має рамку стека, буде обмежено кількість рекурсії, яку ви можете виконати. Нехай ваші ітерабелі є маленькими та / або ледачими, і у вас не повинно виникнути основних проблем. Принаймні більшість проблем, з якими ви зіткнетеся, не потребують повернення мільйона результатів одразу. :)

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


Довга версія:

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

Java розроблялася з самого початку як імперативна , об'єктно-орієнтована мова.

  • Імперативні мови майже завжди залежать від певних змінних змінних. Наприклад, вони прагнуть ітерації щодо рекурсії, і майже всіх ітеративних конструкцій - навіть while (true)і for (;;)! - повністю залежать від змінної, десь змінюється від ітерації до ітерації.
  • Об'єктно-орієнтовані мови в значній мірі передбачають кожну програму як графік об'єктів, що надсилають повідомлення один одному, і майже у всіх випадках реагуючи на ці повідомлення, мутуючи щось.

Кінцевим результатом цих дизайнерських рішень є те, що без змінних змінних Java не може змінити стан нічого - навіть такого простого, як друк "Hello world!" на екран передбачається вихідний потік, який передбачає вклеювання байтів у буфер, що змінюється .

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

class Ints {
     final int value;
     final Ints tail;

     public Ints(int value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints next() { return this.tail; }
     public int value() { return this.value; }
}

public Ints take(int count, Ints input) {
    if (count == 0 || input == null) return null;
    return new Ints(input.value(), take(count - 1, input.next()));
}    

public Ints squares_of(Ints input) {
    if (input == null) return null;
    int i = input.value();
    return new Ints(i * i, squares_of(input.next()));
}

В основному ми будуємо зв'язаний список, де кожен вузол є списком сам по собі. Кожен список має "голову" (поточне значення) та "хвіст" (решту підспису). Більшість функціональних мов роблять щось подібне до цього, оскільки це дуже піддається ефективній незмінності. "Наступна" операція просто повертає хвіст, який, як правило, передається на наступний рівень у стеці рекурсивних викликів.

Тепер це надзвичайно спрощена версія цього матеріалу. Але це досить добре, щоб продемонструвати серйозну проблему з таким підходом на Java. Розглянемо цей код:

public function doStuff() {
    final Ints integers = ...somehow assemble list of 20 million ints...;
    final Ints result = take(25, squares_of(integers));
    ...
}

Хоча нам потрібно лише 25 ints для результату, squares_ofце не знає. Він поверне квадрат кожного числа в integers. Рекурсія на глибину 20 мільйонів рівнів викликає досить великі проблеми на Яві.

Дивіться, функціональні мови, якими ви зазвичай займаєтеся неслухняністю, мають цю функцію, яка називається "усунення хвостових викликів". Що це означає, що коли компілятор бачить, що останнім актом коду є виклик себе (та повернення результату, якщо функція недійсна), він використовує поточний кадр стека поточного виклику замість того, щоб встановити новий, а замість цього зробить "стрибок" "виклику" (тому простір стека залишається незмінним). Коротше кажучи, це проходить приблизно 90% шляху до перетворення хвостової рекурсії в ітерацію. Він міг би мати справу з тими мільярдами точок, не переповнюючи стек. (Зрештою, пам’яті все-таки закінчиться, але складання списку в мільярд ints все одно зіпсує вам пам'ять у 32-бітній системі.)

Java в більшості випадків цього не робить. (Це залежить від компілятора і часу виконання, але реалізація Oracle цього не робить.) Кожен виклик рекурсивної функції з’їдає об'єм пам'яті, що стоїть на кадрі. Використовуйте занадто багато, і ви отримуєте переповнення стека. Переповнення стека все, але гарантує загибель програми. Тому ми повинні переконатися, що цього не робити.

Один напівпроблемний ... лінива оцінка. У нас все ще є обмеження на стек, але вони можуть бути прив'язані до факторів, над якими ми маємо більший контроль. Нам не доведеться обчислювати мільйон ints, щоб повернутись 25. :)

Тож давайте побудуємо нам якусь ліниву оціночну інфраструктуру. (Цей код був перевірений деякий час назад, але я з тих пір я його досить змінив; читайте ідею, а не синтаксичні помилки. :))

// Represents something that can give us instances of OutType.
// We can basically treat this class like a list.
interface Source<OutType> {
     public Source<OutType> next();
     public OutType value();
}

// Represents an operation that turns an InType into an OutType.
// Note, these can be the same type.  We're just flexible like that.
interface Transform<InType, OutType> {
    public OutType appliedTo(InType input);
}

// Represents an action (as opposed to a function) that can run on
// every element of a sequence.
abstract class Action<InType> {
    abstract void doWith(final InType input);
    public void doWithEach(final Source<InType> input) {
        if (input == null) return;
        doWith(input.value());
        doWithEach(input.next());
    }
}

// A list of Integers.
class Ints implements Source<Integer> {
     final Integer value;
     final Ints tail;
     public Ints(Integer value, Ints rest) {
         this.value = value;
         this.tail = rest;
     }
     public Ints(Source<Integer> input) {
         this.value = input.value();
         this.tail = new Ints(input.next());
     }
     public Source<Integer> next() { return this.tail; }
     public Integer value() { return this.value; }
     public static Ints fromArray(Integer[] input) {
         return fromArray(input, 0, input.length);
     }
     public static Ints fromArray(Integer[] input, int start, int end) {
         if (end == start || input == null) return null;
         return new Ints(input[start], fromArray(input, start + 1, end));
     }
}

// An example of the spiff we get by splitting the "iterator" interface
// off.  These ints are effectively generated on the fly, as opposed to
// us having to build a huge list.  This saves huge amounts of memory
// and CPU time, for the rather common case where the whole sequence
// isn't needed.
class Range implements Source<Integer> {
    final int start, end;
    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    public Integer value() { return start; }
    public Source<Integer> next() {
        if (start >= end) return null;
        return new Range(start + 1, end);
    }
}

// This takes each InType of a sequence and turns it into an OutType.
// This *takes* a Transform, rather than just *implementing* Transform,
// because the transforms applied are likely to be specified inline.
// If we just let people override `value()`, we wouldn't easily know what type
// to return, and returning our own type would lose the transform method.
static class Mapper<InType, OutType> implements Source<OutType> {
    private final Source<InType> input;
    private final Transform<InType, OutType> transform;

    public Mapper(Transform<InType, OutType> transform, Source<InType> input) {
        this.transform = transform;
        this.input = input;
    }

    public Source<OutType> next() {
         return new Mapper<InType, OutType>(transform, input.next());
    }
    public OutType value() {
         return transform.appliedTo(input.value());
    }
}

// ...

public <T> Source<T> take(int count, Source<T> input) {
    if (count <= 0 || input == null) return null;
    return new Source<T>() {
        public T value() { return input.value(); }
        public Source<T> next() { return take(count - 1, input.next()); }
    };
}

(Майте на увазі, що якби це справді було життєздатним на Java, код, принаймні дещо подібний до цього, уже входив би в API.)

Тепер, коли створена інфраструктура, досить тривіально писати код, який не потребує змінних змінних і принаймні стабільний для менших обсягів введення.

public Source<Integer> squares_of(Source<Integer> input) {
     final Transform<Integer, Integer> square = new Transform<Integer, Integer>() {
         public Integer appliedTo(final Integer i) { return i * i; }
     };
     return new Mapper<>(square, input);
}


public void example() {
    final Source<Integer> integers = new Range(0, 1000000000);

    // and, as for the author's "bet you can't do this"...
    final Source<Integer> squares = take(25, squares_of(integers));

    // Just to make sure we got it right :P
    final Action<Integer> printAction = new Action<Integer>() {
        public void doWith(Integer input) { System.out.println(input); }
    };
    printAction.doWithEach(squares);
}

Це здебільшого працює, але воно все ще дещо схильне до укладання переливів. Спробуйте take2 мільярди int і зробіть деякі дії над ними. : P Це врешті-решт викине виняток, принаймні, поки 64+ ГБ оперативної пам’яті не стануть стандартними. Проблема полягає в тому, що об'єм пам'яті програми, який зарезервований для її стека, не настільки великий. Зазвичай це від 1 до 8 МБ. (Ви можете попросити більше, але це не має значення , все , що багато , скільки ви просите - ви телефонуєте take(1000000000, someInfiniteSequence), ви будете отримувати виняток.) Контролю До щастя, з ледачими обчисленнями, слабке місце в області , ми можемо краще . Треба просто бути обережними, скільки ми take().

З масштабуванням масштабів все ще буде багато проблем, оскільки використання нашої стеки лінійно збільшується. Кожен дзвінок обробляє один елемент, а решту передає іншому. Тепер, коли я думаю про це, однак, є одна хитрість, яку ми можемо досягти, і це може принести нам трохи більше місця: перетворити ланцюжок дзвінків у дерево дзвінків Розглянемо щось подібне:

public <T> void doSomethingWith(T input) { /* magic happens here */ }
public <T> Source<T> workWith(Source<T> input, int count) {
    if (count < 0 || input == null) return null;
    if (count == 0) return input;
    if (count == 1) {
        doSomethingWith(input.value());
        return input.next();
    }
    return (workWith(workWith(input, count/2), count - count/2);
}

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

Проблема полягає в тому, що ця функція бажає введення даних - і при пов'язаному списку отримання довжини вимагає проходження всього списку. Це легко вирішити, хоча; просто не байдуже, скільки записів є. :) Наведений вище код буде працювати з чимось на зразок Integer.MAX_VALUEпідрахунку, оскільки нуль зупиняє обробку в будь-якому випадку. Кількість в основному є, тому у нас є міцний базовий випадок. Якщо ви очікуєте, що Integer.MAX_VALUEв списку буде більше записів, то ви можете перевірити workWithповернене значення - воно повинно бути нульовим в кінці. В іншому випадку повторіть.

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


3

Раніше я намагався створити інтерпретатор для мови, що нагадує lisp, на Java (кілька років тому і весь код був втрачений, як це було в CVS на sourceforge), і ітератори Java-утилі трохи довільні для функціонального програмування за списками.

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

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

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

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

Суть square ( int x )функції полягає лише в тому, щоб зняти необхідність виклику head()двічі - зазвичай я би це зробив, додавши значення в остаточну змінну, але додавання цієї функції означає, що в програмі немає змінних, а лише параметри функції.

Багатомовність Java для такого роду програмувань змусила мене написати натомість другу версію мого інтерпретатора на C99.

public class Squares {
    interface Seq<T> {
        T head();
        Seq<T> tail();
    }

    public static void main (String...args) {
        print ( take (25, integers ) );
        print ( take (25, squaresOf ( integers ) ) );
        print ( take (25, squaresOfUsingMap ( integers ) ) );
    }

    static Seq<Integer> CreateIntSeq ( final int n) {
        return new Seq<Integer> () {
            public Integer head () {
                return n;
            }
            public Seq<Integer> tail () {
                return n > 0 ? CreateIntSeq ( -n ) : CreateIntSeq ( 1 - n );
            }
        };
    }

    public static final Seq<Integer> integers = CreateIntSeq(0);

    public static Seq<Integer> squaresOf ( final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return square ( source.head() );
            }
            public Seq<Integer> tail () {
                return squaresOf ( source.tail() );
            }
        };
    }

    // mapping a function over a list rather than implementing squaring of each element
    interface Fun<T> {
        T apply ( T value );
    }

    public static Seq<Integer> squaresOfUsingMap ( final Seq<Integer> source ) {
        return map ( new Fun<Integer> () {
            public Integer apply ( final Integer value ) {
                return square ( value );
            }
        }, source );
    }

    public static <T> Seq<T> map ( final Fun<T> fun, final Seq<T> source ) {
        return new Seq<T> () {
            public T head () {
                return fun.apply ( source.head() );
            }
            public Seq<T> tail () {
                return map ( fun, source.tail() );
            }
        };
    }

    public static Seq<Integer> take ( final int count,  final Seq<Integer> source ) {
        return new Seq<Integer> () {
            public Integer head () {
                return source.head();
            }
            public Seq<Integer> tail () {
                return count > 0 ? take ( count - 1, source.tail() ) : nil;
            }
        };
    }

    public static int square ( final int x ) {
        return x * x;
    }

    public static final Seq<Integer> nil = new Seq<Integer> () {
        public Integer head () {
            throw new RuntimeException();
        }
        public Seq<Integer> tail () {
            return this;
        }
    };

    public static <T> void print ( final Seq<T> seq ) {
        printPartSeq ( "[", seq.head(), seq.tail() );
    }

    private static <T> void printPartSeq ( final String prefix, final T value, final Seq<T> seq ) {
        if ( seq == nil) {
            System.out.println("]");
        } else {
            System.out.print(prefix);
            System.out.print(value);
            printPartSeq ( ",", seq.head(), seq.tail() );
        }
    }
}

3

Як писати корисні програми Java без використання змінних змінних.

Теоретично ви можете реалізувати практично все на Java, використовуючи лише рекурсію та без змінних змінних.

На практиці:

  • Мова Java не призначена для цього. Багато конструкцій розроблені для мутації, і без них важко використовувати. (Наприклад, ви не можете ініціалізувати масив Java змінної довжини без мутації.)

  • Дітто для бібліотек. А якщо ви обмежитеся бібліотечними класами, які не використовують мутації під обкладинкою, це ще важче. (Ви навіть не можете використовувати String ... подивіться, як hashcodeце реалізовано.)

  • Основні реалізації Java не підтримують оптимізацію хвостових викликів. Це означає, що рекурсивні версії алгоритмів, як правило, залишають простір стеків "голодним". А оскільки стеки потоків Java не зростають, вам потрібно виділити великі стеки ... або ризикувати StackOverflowError.

Поєднайте ці три речі, і Java насправді не є життєздатним варіантом для написання корисних (тобто нетривіальних) програм без змінних змінних.

(Але так, це нормально. Для JVM доступні інші мови програмування, деякі з яких підтримують функціональне програмування.)


2

Коли ми шукаємо приклад концепцій, я б сказав, давайте відкладемо Java і шукатимемо інше, але звичне налаштування, в якому можна знайти знайомий варіант понять. Труби UNIX досить схожі на прив'язування лінивих функцій.

cat /dev/zero | tr '\0' '\n' | cat -n | awk '{ print $0 * $0 }' | head 25

У Linux це означає, дайте мені байти, кожен з яких складається з помилкових, а не справжніх бітів, поки я не втрачу апетит; змінити кожен з цих байтів на новий рядок; нумерувати кожен створений таким чином рядок; і вивести квадрат із цього числа. Крім того, у мене апетит на 25 рядків і не більше.

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

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

З іншого боку, я стверджую, що наступний програміст може більше сприймати, якщо деякі з наших Java-пакетів насправді є пакетами Java Virtual Machine, написаними на одній з функціональних або об'єктно-функціональних мов, таких як Clojure та Scala. Вони розроблені так, щоб кодуватися ланцюжком функцій разом і викликатися з Java в звичайному порядку дзвінків методів Java.

Знову ж таки, для програміста Java може бути хорошою ідеєю взяти натхнення від функціонального програмування місцями.

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

int f(final int n) {
    final int result; // not initialized here!
    if (n < 0) {
        result = -n;
    } else if (n < 1) {
        result = 0;
    } else {
        result = n - 1;
    }
    // If I would leave off the "else" clause,
    // Java would fail to compile complaining that
    // "result" is possibly uninitialized.
    return result;
}


Я приблизно на 70% впевнений, що Java вже перевіряє повернене значення. Ви повинні отримати помилку про "відсутність заяви повернення", якщо контроль може відпасти від кінця недійсної функції.
cHao

Моя думка: Якщо ви кодуєте це так, як int result = -n; if (n < 1) { result = 0 } return result;він компілюється просто чудово, і компілятор не має уявлення про те, чи ви мали намір зробити його еквівалентним функції в моєму прикладі. Можливо, цей приклад занадто простий, щоб зробити техніку вигідною, але у функції з великою кількістю гілок мені здається зрозуміти, що результат призначається рівно один раз, незалежно від того, який шлях слід.
мінопрет

Якщо ви скажете if (n < 1) return 0; else return -n;, проте, у вас закінчиться без проблем ... і до того ж простіше. :) Мені здається, що в такому випадку правило "одного повернення" насправді допомагає викликати питання про невідомість, коли встановлено ваше повернене значення; в іншому випадку ви можете просто повернути його, і Java зможе визначити, коли інші шляхи можуть не повернути значення, тому що ви більше не розлучаєтеся з обчисленням значення від фактичного повернення його.
cHao

Або, наприклад , ваша відповідь, в if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;.
cHao

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

0

Найпростіший спосіб виявити, що це буде подавати наступне в компілятор Frege , і подивитися на створений код Java:

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x

Через кілька днів я виявив, що думки повертаються до цієї відповіді. Зрештою, моя частина пропозицій полягала в реалізації функціональних частин програмування в Scala. Якщо ми розглядаємо можливість застосування Scala в тих місцях, де нас справді мав на увазі Хаскелл (і я думаю, що я не єдиний blog.zlemma.com/2013/02/20/… ), чи не слід було б принаймні вважати Фреге?
мінопрет

@minopret Це справді та ніша, яку Фреге замикає - люди, які пізнали і люблять Хаскелл і все-таки потребують в JVM. Впевнений, одного дня Фреге буде достатньо зрілим, щоб хоча б серйозно розглянути.
Інго
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.