Jak pisać użyteczne programy Java bez użycia zmiennych zmiennych

12

Czytałem artykuł o programowaniu funkcjonalnym, w którym pisze autor

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

Zauważ, że nie ma żadnych zmiennych. Rzeczywiście, ma tylko trzy funkcje i jedną stałą. Spróbuj napisać kwadraty liczb całkowitych w Javie bez użycia zmiennej. Och, prawdopodobnie jest na to sposób, ale z pewnością nie jest to naturalne i nie przeczytałoby tak ładnie, jak mój program powyżej.

Czy można to osiągnąć w Javie? Zakładając, że musisz wydrukować kwadraty pierwszych 15 liczb całkowitych, czy możesz napisać pętlę for lub while bez używania zmiennych?

Powiadomienie o modzie

To pytanie nie jest konkursem golfowym. Szukamy odpowiedzi, które wyjaśniają związane z nimi pojęcia (najlepiej bez powtarzania wcześniejszych odpowiedzi), a nie tylko dla kolejnego fragmentu kodu.

minusSeven
źródło
19
Twój przykład funkcjonalne robi zmienne użytku wewnątrz, ale język robi to wszystko za kulisami. Skutecznie przekazałeś nieprzyjemne części osobie, która Twoim zdaniem zrobiła to poprawnie.
Blrfl,
12
@Blrfl: Argument „za kulisami” zabija wszystkie debaty oparte na języku, ponieważ każdy fragment kodu jest ostatecznie tłumaczony na kod maszynowy x86. Kod x86 nie jest obiektowy, nie proceduralny, nie działa, nic, ale te kategorie są cennymi znacznikami dla języków programowania. Spójrz na język, a nie na implementację.
thiton
10
@thiton Disagreed. Blrfl mówi, że funkcje te prawdopodobnie wykorzystują zmienne napisane w tym samym języku programowania . Nie musisz tutaj schodzić na niższy poziom. Fragment korzysta jedynie z funkcji bibliotecznych. Możesz łatwo napisać ten sam kod w Javie, patrz: squaresOf(integers()).take(25)(pisanie tych funkcji pozostawia się jako ćwiczenie dla czytelnika; trudność leży w nieskończonym zestawie integers(), ale jest to problem dla Javy ze względu na jego chętną ocenę, nie ma nic wspólnego z zmienne)
Andres F.,
6
Ten cytat jest mylący i mylący, nie ma tam magii, tylko cukier składniowy .
yannis
2
@thiton Sugeruję, aby dowiedzieć się więcej o językach FP, jednak fragment kodu nie obsługuje (lub odrzuca) twierdzenia, że ​​„zmienne” nie są potrzebne (przez co zakładam, że masz na myśli „zmienne zmienne”, ponieważ inne rodzaj jest powszechny w FP). Fragment pokazuje tylko funkcje biblioteki, które mogły być zaimplementowane również w Javie, wykluczając leniwe / chętne problemy, które są tutaj nie na temat.
Andres F.,

Odpowiedzi:

31

Czy można zaimplementować taki przykład w Javie bez użycia destrukcyjnych aktualizacji? Tak. Jednak, jak wspomniano @Titon i sam artykuł, będzie brzydki (w zależności od gustu). Jednym ze sposobów jest użycie rekurencji; oto przykład Haskella, który robi coś podobnego:

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

Uwaga 1) brak mutacji, 2) zastosowanie rekurencji oraz 3) brak pętli. Ostatnia kwestia jest bardzo ważna - języki funkcjonalne nie wymagają wbudowanych w język konstrukcji zapętlonych, ponieważ rekurencja może być używana w większości (wszystkich?) Przypadków, w których pętle są używane w Javie. Oto dobrze znana seria artykułów pokazujących, jak niezwykle ekspresyjne mogą być wywołania funkcji.


Ten artykuł był dla mnie niezadowalający i chciałbym przedstawić kilka dodatkowych uwag:

Ten artykuł jest bardzo kiepskim i mylącym wyjaśnieniem programowania funkcjonalnego i jego zalet. Zdecydowanie poleciłbym inne źródła do nauki o programowaniu funkcjonalnym.

Najbardziej mylącą częścią tego artykułu jest to, że nie wspomina, że ​​istnieją dwa zastosowania instrukcji przypisania w Javie (i większości innych głównych języków):

  1. powiązanie wartości z nazwą: final int MAX_SIZE = 100;

  2. aktualizacja destrukcyjna: int a = 3; a += 1; a++;

Programowanie funkcjonalne omija drugie, ale obejmuje pierwsze (przykłady: let-wyrażenia, parametry funkcji, itiony najwyższego poziomu define) . Jest to bardzo ważny punkt do uchwycenia, ponieważ w przeciwnym razie po prostu artykuł wydaje się głupie, a może zostawić cię zastanawiać, jakie są take, squares-ofi integersjeśli nie zmienne?

Ponadto przykład nie ma znaczenia. To nie pokazuje implementacje take, squares-oflub integers. Z tego, co wiemy, są one implementowane przy użyciu zmiennych zmiennych. Jak powiedział @Martin, możesz w prosty sposób napisać ten przykład w Javie.

Jeszcze raz polecam unikanie tego artykułu i innych podobnych, jeśli naprawdę chcesz dowiedzieć się o programowaniu funkcjonalnym. Wydaje się, że jest napisane bardziej w celu szokowania i obrażania niż nauczania pojęć i podstaw. Zamiast tego sprawdź jeden z moich ulubionych artykułów Johna Hughesa. Hughes próbuje rozwiązać niektóre z tych samych problemów, które omówił artykuł (chociaż Hughes nie mówi o współbieżności / równoległości); oto zwiastun:

Ten artykuł jest próbą wykazania większej społeczności (niefunkcjonalnych) programistów znaczenia programowania funkcjonalnego, a także pomocy funkcjonalnym programistom w pełnym wykorzystaniu ich zalet poprzez wyjaśnienie, jakie są te zalety.

[...]

W dalszej części tego artykułu będziemy argumentować, że języki funkcjonalne zapewniają dwa nowe, bardzo ważne rodzaje kleju. Podamy przykłady programów, które można modulować na nowe sposoby, a tym samym można uprościć. Jest to klucz do mocy funkcjonalnego programowania - umożliwia lepszą modularyzację. Jest to również cel, do którego muszą dążyć funkcjonalni programiści - mniejsze, prostsze i bardziej ogólne moduły, sklejone z nowymi klejami, które opiszemy.


źródło
10
+1 za „Polecam unikanie tego artykułu i innych podobnych, jeśli naprawdę chcesz dowiedzieć się o programowaniu funkcjonalnym. Wydaje się, że jest napisane bardziej w celu szokowania i obrażania niż nauczania pojęć i podstaw”.
3
Połowa powodów, dla których ludzie nie robią FP, to fakt, że nie słyszą / nie uczą się niczego na ten temat w uni, a druga połowa to dlatego, że kiedy się na nie patrzą, znajdują artykuły, które pozostawiają ich zarówno niedoinformowanymi, jak i myślącymi, że to wszystko dla fantazji bawienie się, a nie przemyślane, uzasadnione podejście z korzyściami. +1 za podanie lepszych źródeł informacji
Jimmy Hoffa
3
Umieść odpowiedź na pytanie u góry, jeśli tak, to bardziej bezpośrednio do pytania i być może to pytanie pozostanie otwarte (z bezpośrednią odpowiedzią ukierunkowaną na pytanie)
Jimmy Hoffa
2
Przepraszam za nitpick, ale nie rozumiem, dlaczego wybrałeś ten kod haskell. Przeczytałem LYAH i wasz przykład jest trudny do zrozumienia. Nie widzę też związku z pierwotnym pytaniem. Dlaczego nie wykorzystałeś tego take 25 (map (^2) [1..])jako przykładu?
Daniel Kaplan
2
@tieTYT dobre pytanie - dziękuję za zwrócenie na to uwagi. Powodem, dla którego użyłem tego przykładu jest to, że pokazuje on, jak wygenerować listę liczb przy użyciu rekurencji i unikania zmiennych zmiennych. Moim zamiarem było, aby OP zobaczył ten kod i pomyślał o tym, jak zrobić coś podobnego w Javie. Czym jest fragment kodu [1..]? To fajna funkcja wbudowana w Haskell, ale nie ilustruje koncepcji generowania takiej listy. Jestem pewien, że instancje Enumklasy (której wymaga ta składnia) są również pomocne, ale były zbyt leniwe, aby je znaleźć. Tak więc unfoldr. :)
27

Nie zrobiłbyś tego. Zmienne są podstawą programowania imperatywnego, a jeśli spróbujesz programować imperatywnie bez użycia zmiennych, po prostu sprawisz każdemu ból w dupie. W różnych paradygmatach programowania style są różne, a różne koncepcje stanowią podstawę. Zmienna w Javie, gdy jest dobrze używana z małym zakresem, nie jest złem. Pytanie o program Java bez zmiennych jest jak proszenie o program Haskell bez funkcji, więc nie pytaj o to i nie daj się oszukać, że programowanie imperatywne jest gorsze, ponieważ używa zmiennych.

Tak więc sposobem Java byłoby:

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

i nie daj się zwieść napisaniu go w bardziej złożony sposób z powodu nienawiści do zmiennych.

Thiton
źródło
5
„Nienawiść do zmiennych”? Ooookay ... Co czytałeś o programowaniu funkcjonalnym? Jakie języki wypróbowałeś? Które tutoriale?
Andres F.,
8
@AndresF .: Ponad dwa lata zajęć w Haskell. Nie twierdzę, że FP jest zły. Jednak w wielu dyskusjach FP-vs.-IP (takich jak artykuł powiązany) istnieje tendencja do potępienia korzystania z nazwanych podmiotów, które można ponownie przypisać (zmienne AKA), oraz do potępienia bez uzasadnionego powodu lub danych. Nieuzasadnione potępienie jest nienawiścią w mojej książce. A nienawiść powoduje naprawdę zły kod.
thiton
10
„Nienawiść do zmiennych” jest nadmiernym uproszczeniem przyczynowym en.wikipedia.org/wiki/Fallacy_of_the_single_cause, ponieważ istnieje wiele korzyści z programowania bezstanowego, które można nawet uzyskać w Javie, choć zgadzam się z twoją odpowiedzią, że w Javie koszt byłby zbyt skomplikowany, aby program i nie jest idiomatyczny. Nadal nie przestawałbym wymachiwać ręką myślą, że programowanie bezstanowe jest dobre, a stanowe jest złe, ponieważ jest to reakcja emocjonalna, a nie uzasadniona, dobrze przemyślana postawa, którą ludzie przyjęli z powodu doświadczenia.
Jimmy Hoffa
2
Zgodnie z tym, co mówi @JimmyHoffa, odsyłam cię do Johna Carmacka na temat programowania funkcjonalnego w imperatywnych językach (w tym przypadku C ++) ( altdevblogaday.com/2012/04/26/functional-programming-in-c ).
Steven Evers,
5
Nieuzasadnione potępienie nie jest nienawiścią, a unikanie stanu zmiennego nie jest nierozsądne.
Michael Shaw
21

Najprostsze, co mogę zrobić z rekurencją, to funkcja z jednym parametrem. Nie jest bardzo Java-esque, ale działa:

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);
        }
    }
}
FrustratedWithFormsDesigner
źródło
3
+1 za próbę odpowiedzi na pytanie za pomocą przykładu Java.
KChaloux
Oddałbym to za prezentację kodu w stylu golfa (patrz uwaga Mod ), ale nie mogę zmusić się do naciśnięcia strzałki w dół, ponieważ ten kod doskonale pasuje do stwierdzeń zawartych w mojej ulubionej odpowiedzi : „1) brak mutacji, 2) użycie rekurencja i 3) brak pętli ”
komnata
3
@gnat: Ta odpowiedź została opublikowana przed powiadomieniem Mod. Nie chciałem mieć świetnego stylu, chciałem uprościć i zaspokoić pierwotne pytanie OP; aby pokazać, że to jest możliwe, aby robić takie rzeczy w Javie.
FrustratedWithFormsDesigner
@FrustratedWithFormsDesigner na pewno; nie powstrzymałoby mnie to przed nagrywaniem (ponieważ powinieneś być w stanie edytować, aby zachować zgodność) - to uderzająco idealne dopasowanie, które zrobiło magię. Dobra robota, naprawdę dobra robota, dość pouczająca - dziękuję
komnata
16

W twoim przykładzie funkcjonalnym nie widzimy, jak implementowane są funkcje squares-ofi take. Nie jestem ekspertem od Java, ale jestem pewien, że moglibyśmy napisać te funkcje, aby włączyć takie zdanie ...

squares_of(integers).take(25);

co nie jest tak bardzo różne.

Jaskółka oknówka
źródło
6
Nitpick: squares-ofnie jest prawidłową nazwą w Javie ( squares_ofchoć jest). Ale poza tym dobra uwaga, która pokazuje, że przykład tego artykułu jest słaby.
Podejrzewam, że artykuł integerleniwie generuje liczby całkowite, a takefunkcja wybiera 25 squared-ofliczb z integer. Krótko mówiąc, powinieneś mieć integerfunkcję leniwego tworzenia liczb całkowitych do nieskończoności.
Onesimus Bez ograniczeń
Wywoływanie czegoś takiego jak (integer)funkcja jest trochę szaleństwem - funkcja wciąż jest czymś, co mapuje argument na wartość. Okazuje się, że (integer)nie jest to funkcja, a jedynie wartość. Można nawet posunąć się tak daleko, aby powiedzieć, że integerjest to zmienna związana z nieskończoną liczbą liczb.
Ingo
6

W Javie możesz to zrobić (zwłaszcza nieskończoną część listy) za pomocą iteratorów. W poniższym przykładzie kodu liczba dostarczona do Takekonstruktora może być dowolnie wysoka.

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());
    }
}

Lub metodami fabrycznymi z możliwością łączenia:

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

Gdzie SquaresOf, Takei Integersprzedłużyć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() {}
}
artistoex
źródło
1
To pokazuje wyższość paradygmatu OO nad funkcjonalnym; z odpowiednim projektem OO można naśladować paradygmat funkcjonalny, ale nie można naśladować paradygmatu OO w funkcjonalnym stylu.
m3th0dman
3
@ m3th0dman: Przy odpowiednim projekcie OO możesz naśladować FP na pół- podobnie jak każdy język, który ma ciągi, listy i / lub słowniki na pół naśladować OO. Równoważność Turinga języków ogólnego przeznaczenia oznacza, że ​​przy wystarczającym wysiłku każdy język może symulować cechy dowolnego innego.
cHao
Zauważ, że iteratory w stylu Java, takie jak in, while (test.hasNext()) System.out.println(test.next())byłyby nie do przyjęcia w FP; iteratory są z natury zmienne.
cHao
1
@ cHao Nie wierzę, że można naśladować prawdziwą enkapsulację lub polimorfizm; także Java (w tym przykładzie) nie może naśladować funkcjonalnego języka ze względu na ścisłą, chętną ocenę. Uważam również, że iteratory można pisać rekurencyjnie.
m3th0dman
@ m3th0dman: Polimorfizm wcale nie będzie trudny do naśladowania; potrafi to zrobić nawet język C i asembler. Po prostu ustaw metodę na pole w obiekcie lub deskryptor klasy / vtable. A enkapsulacja w sensie ukrywania danych nie jest ściśle potrzebna; połowa języków tam go nie zapewnia, gdy twój obiekt jest niezmienny, nie ma znaczenia, czy ludzie i tak zobaczą jego wnętrzności. wszystko, czego potrzeba, to owijanie danych , które z łatwością mogłyby zapewnić wspomniane pola metod.
cHao
6

Krótka wersja:

Aby styl pojedynczego przydziału działał niezawodnie w Javie, potrzebujesz (1) pewnego rodzaju niezmiennej infrastruktury i (2) wsparcia na poziomie kompilatora lub środowiska wykonawczego w celu eliminacji wywołania ogonowego.

Możemy napisać dużą część infrastruktury i możemy zorganizować rzeczy, aby uniknąć zapełniania stosu. Ale tak długo, jak każde wywołanie zajmuje ramkę stosu, będzie istniało ograniczenie liczby możliwych rekurencji. Staraj się, aby iterery były małe i / lub leniwe, i nie powinieneś mieć większych problemów. Przynajmniej większość problemów, na które napotkasz, nie wymaga natychmiastowego powrotu miliona wyników. :)

Pamiętaj również, że ponieważ program musi faktycznie wprowadzać widoczne zmiany, aby być wartym uruchomienia, nie można sprawić, by wszystko było niezmienne. Możesz jednak zachować ogromną większość własnych rzeczy na niezmiennym poziomie, używając niewielkiego podzbioru niezbędnych zmiennych (na przykład strumieni) tylko w niektórych kluczowych punktach, w których alternatywy byłyby zbyt uciążliwe.


Długa wersja:

Mówiąc najprościej, program Java nie może całkowicie ominąć zmiennych, jeśli chce zrobić coś, co warto zrobić. Można zawierać je, a tym samym ograniczyć zmienność na ogromnym stopniu, ale bardzo zaprojektować języka i API - wraz z potrzebą, aby w końcu zmienić system podstawowy - dokonać całkowitej niezmienności niewykonalne.

Java została zaprojektowana od początku jako imperatywny , zorientowany obiektowo język.

  • Języki imperatywne prawie zawsze zależą od zmiennych zmiennych. Na przykład preferują iterację nad rekurencją i prawie wszystkie konstrukty iteracyjne - nawet while (true)i for (;;)! - są całkowicie zależne od zmiennej, która zmienia się gdzieś z iteracji na iterację.
  • Języki zorientowane obiektowo właściwie wyobrażają sobie każdy program jako wykres obiektów wysyłających do siebie wiadomości, a prawie we wszystkich przypadkach, odpowiadających na te wiadomości poprzez mutowanie czegoś.

Efektem końcowym tych decyzji projektowych jest to, że bez zmiennych zmiennych Java nie ma możliwości zmiany stanu czegokolwiek - nawet czegoś tak prostego jak wydruk „Witaj świecie!” do ekranu wiąże się strumień wyjściowy, który polega na umieszczaniu bajtów w buforze , który można modyfikować .

Tak więc dla wszystkich praktycznych celów ograniczamy się do usuwania zmiennych z własnego kodu. OK, możemy to zrobić. Prawie. Zasadniczo potrzebowalibyśmy zastąpić prawie całą iterację rekurencją, a wszystkie mutacje rekurencyjnymi wywołaniami zwracającymi zmienioną wartość. tak jak ...

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()));
}

Zasadniczo budujemy listę połączoną, w której każdy węzeł jest listą samą w sobie. Każda lista ma „nagłówek” (bieżąca wartość) i „ogon” (pozostała lista podrzędna). Większość języków funkcjonalnych robi coś podobnego, ponieważ jest bardzo podatna na niezmienność. „Następna” operacja po prostu zwraca ogon, który zwykle jest przekazywany do następnego poziomu w stosie wywołań rekurencyjnych.

To jest bardzo uproszczona wersja tych rzeczy. Ale to wystarczy, aby zademonstrować poważny problem z tym podejściem w Javie. Rozważ ten kod:

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

Chociaż do wyniku potrzebujemy tylko 25 liczb wewnętrznych, squares_ofnie wie o tym. Zwróci kwadrat każdej liczby w integers. Rekurencja na głębokości 20 milionów poziomów powoduje całkiem duże problemy w Javie.

Widzisz, funkcjonalne języki, w których zwykle robisz takie dziwactwa, mają funkcję zwaną „eliminacją ogona”. Oznacza to, że gdy kompilator widzi, że ostatnim działaniem kodu jest wywołanie samego siebie (i zwrócenie wyniku, jeśli funkcja nie jest nieważna), używa ramki stosu bieżącego wywołania zamiast konfigurowania nowego i wykonuje zamiast tego „skok” „wywołania” (więc używane miejsce na stosie pozostaje stałe). Krótko mówiąc, idzie to w około 90% w kierunku przekształcenia rekurencji ogona w iterację. Może poradzić sobie z tymi miliardami ints bez przepełnienia stosu. (W końcu wciąż zabraknie pamięci, ale zestawienie miliarda ints i tak zepsuje pamięć w systemie 32-bitowym).

W większości przypadków Java tego nie robi. (Zależy to od kompilatora i środowiska wykonawczego, ale implementacja Oracle tego nie robi.) Każde wywołanie funkcji rekurencyjnej zużywa pamięć ramki stosu. Zużyj za dużo, a otrzymasz przepełnienie stosu. Przepełnienie stosu prawie gwarantuje śmierć programu. Musimy więc upewnić się, że tego nie zrobimy.

Jedno pół obejście ... leniwa ocena. Nadal mamy ograniczenia dotyczące stosów, ale można je powiązać z czynnikami, nad którymi mamy większą kontrolę. Nie musimy obliczać miliona ints tylko po to, aby zwrócić 25. :)

Zbudujmy więc trochę leniwej infrastruktury do oceny. (Ten kod został przetestowany jakiś czas temu, ale od tego czasu go trochę zmodyfikowałem; przeczytaj pomysł, a nie błędy składniowe. :))

// 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()); }
    };
}

(Należy pamiętać, że jeśli byłoby to faktycznie wykonalne w Javie, kod przynajmniej nieco podobny do powyższego byłby już częścią API).

Teraz, gdy istnieje infrastruktura, pisanie kodu, który nie wymaga zmiennych i jest co najmniej stabilny przy mniejszych ilościach danych, jest dość trywialny.

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);
}

To głównie działa, ale wciąż jest podatne na przepełnienia stosu. Wypróbuj take2 miliardy intów i wykonaj na nich jakąś akcję. : P W końcu wyrzuci wyjątek, przynajmniej dopóki 64+ GB pamięci RAM nie stanie się standardem. Problem polega na tym, że ilość pamięci programu zarezerwowana dla stosu nie jest tak duża. Zazwyczaj wynosi od 1 do 8 MiB. (Można poprosić o większe, ale to nie ma znaczenia aż tak dużo, ile prosisz - dzwonisz take(1000000000, someInfiniteSequence), to będzie uzyskać wyjątek.) Kontrolę szczęście z oceny leniwy, słabym punktem jest w obszarze możemy lepiej . Musimy tylko uważać, ile my take().

Nadal będzie miał wiele problemów ze skalowaniem, ponieważ nasze zużycie stosu rośnie liniowo. Każde połączenie obsługuje jeden element, a resztę przekazuje do innego połączenia. Teraz, gdy o tym myślę, jest jedna sztuczka, którą możemy wyciągnąć, która może dać nam nieco więcej rezerwy: zmienić łańcuch połączeń w drzewo połączeń. Zastanów się nad czymś takim:

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);
}

workWithw zasadzie dzieli pracę na dwie połowy i przypisuje sobie każdą połowę do innego wezwania. Ponieważ każde wywołanie zmniejsza rozmiar listy roboczej o połowę, a nie o jedną, powinno to być skalowane logarytmicznie, a nie liniowo.

Problem polega na tym, że ta funkcja potrzebuje danych wejściowych - a przy połączonej liście uzyskanie długości wymaga przejścia całej listy. Łatwo to jednak rozwiązać; po prostu nie obchodzi mnie, ile jest wpisów. :) Powyższy kod działałby z czymś takim Integer.MAX_VALUEjak liczenie, ponieważ null i tak zatrzymuje przetwarzanie. Liczba jest głównie tam, więc mamy solidną podstawę. Jeśli spodziewasz się, że Integer.MAX_VALUEna liście będzie więcej niż kilka pozycji, możesz sprawdzić workWithwartość zwracaną - na końcu powinna ona być pusta. W przeciwnym razie powtórz.

Pamiętaj, że dotyka to tyle elementów, ile chcesz. To nie jest leniwe; robi to natychmiast. Chcesz to zrobić tylko dla akcji - to znaczy dla rzeczy , których jedynym celem jest zastosowanie się do każdego elementu na liście. W tej chwili myślę, że sekwencje byłyby o wiele mniej skomplikowane, gdyby były liniowe; nie powinno stanowić problemu, ponieważ sekwencje i tak nie nazywają się same - po prostu tworzą obiekty, które wywołują je ponownie.

cHao
źródło
3

Wcześniej próbowałem utworzyć interpreter dla języka podobnego do seplenienia w Javie (kilka lat temu i cały kod został utracony, tak jak w CVS w sourceforge), a iteratory Java są nieco gadatliwe dla programowania funkcjonalnego na listach.

Oto coś oparte na interfejsie sekwencji, który ma tylko dwie operacje potrzebne do uzyskania bieżącej wartości i uzyskania sekwencji zaczynającej się od następnego elementu. Są one nazywane głową i ogonem po funkcjach w schemacie.

Ważne jest, aby używać mniej więcej tak Seqlub Iteratorinterfejsów jak oznacza to, że lista jest tworzona leniwie. IteratorInterfejs nie może być niezmienny przedmiot, więc jest mniej nadaje się do programowania funkcyjnego - jeśli nie można stwierdzić, czy wartość przekazać do funkcji został zmieniony przez nią, traci jedną z kluczowych zalet programowania funkcjonalnego.

Oczywiście integerspowinna być lista wszystkich liczb całkowitych, więc zacząłem od zera i na przemian zwracałem wartości dodatnie i ujemne.

Istnieją dwie wersje kwadratów - jedna tworzy niestandardową sekwencję, druga używa map„funkcji” - Java 7 nie ma lambda, więc użyłem interfejsu - i stosuje ją kolejno do każdego elementu w sekwencji.

Celem tej square ( int x )funkcji jest jedynie usunięcie potrzeby head()dwukrotnego wywoływania - normalnie zrobiłbym to, umieszczając wartość w zmiennej końcowej, ale dodanie tej funkcji oznacza, że ​​w programie nie ma żadnych zmiennych, tylko parametry funkcji.

Gadatliwość Java dla tego rodzaju programowania skłoniła mnie do napisania drugiej wersji mojego interpretera w 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() );
        }
    }
}
Pete Kirkham
źródło
3

Jak pisać użyteczne programy Java bez użycia zmiennych zmiennych.

Teoretycznie możesz zaimplementować prawie wszystko w Javie, używając tylko rekurencji i żadnych zmiennych zmiennych.

W praktyce:

  • Język Java nie jest do tego przeznaczony. Wiele konstruktów zaprojektowano pod kątem mutacji i bez nich trudno je stosować. (Na przykład nie można zainicjować tablicy Java o zmiennej długości bez mutacji).

  • To samo dotyczy bibliotek. A jeśli ograniczysz się do klas bibliotek, które nie wykorzystują mutacji pod przykryciem, jest to jeszcze trudniejsze. (Nie możesz nawet użyć String ... zobacz, jak hashcodejest implementowany.)

  • Implementacje Java głównego nurtu nie obsługują optymalizacji wywołania ogona. Oznacza to, że rekurencyjne wersje algorytmów są „głodne” przestrzeni stosu. A ponieważ stosy wątków Java nie rosną, musisz wstępnie przydzielić duże stosy ... lub zaryzykować StackOverflowError.

Połącz te trzy rzeczy, a Java nie jest tak naprawdę realną opcją do pisania użytecznych (tzn. Nietrywialnych) programów bez zmiennych.

(Ale hej, to w porządku. Istnieją inne języki programowania dla JVM, z których niektóre obsługują programowanie funkcjonalne.)

Stephen C.
źródło
2

Gdy szukamy przykładu pojęć, powiedziałbym, odłóżmy na bok Javę i poszukajmy innego, ale znanego ustawienia, w którym można znaleźć znaną wersję pojęć. Potoki UNIX są raczej podobne do łączenia leniwych funkcji.

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

W Linuksie oznacza to, daj mi bajty, z których każdy składa się z fałszywych, a nie prawdziwych bitów, dopóki nie stracę apetytu; zmień każdy z tych bajtów na znak nowej linii; numer każdej tak utworzonej linii; i wyprodukuj kwadrat tej liczby. Ponadto mam apetyt na 25 linii i nie więcej.

Twierdzę, że programista nie powinien odradzać pisania potoku Linux w ten sposób. To stosunkowo normalne skrypty powłoki Linux.

Twierdzę, że programista nie powinien próbować pisać tego samego podobnie w Javie. Powodem jest utrzymanie oprogramowania jako główny czynnik w całym cyklu życia projektów oprogramowania. Nie chcemy zamykać następnego programisty, prezentując pozornie program Java, ale tak naprawdę jest on napisany w niestandardowym, jednorazowym języku poprzez skomplikowane kopiowanie funkcjonalności, która już istnieje na platformie Java.

Z drugiej strony twierdzę, że następny programista mógłby być bardziej akceptowalny, gdyby niektóre nasze pakiety „Java” były faktycznie pakietami wirtualnej maszyny Java napisanymi w jednym z języków funkcjonalnych lub obiektowych / funkcjonalnych, takich jak Clojure i Scala. Są one zaprojektowane do kodowania przez łączenie funkcji razem i do wywoływania z Javy w normalny sposób wywołań metod Java.

Z drugiej strony programista Java może nadal być inspiracją do programowania funkcjonalnego.

Ostatnio moją ulubioną techniką było użycie niezmiennej, niezainicjowanej zmiennej powrotu i pojedynczego wyjścia, aby, podobnie jak niektóre kompilatory języka funkcjonalnego, Java sprawdzała, czy bez względu na to, co dzieje się w treści funkcji, zawsze zapewniam jedną i tylko jedną zwracana wartość. Przykład:

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;
}

minopret
źródło
Mam około 70% pewności, że Java już i tak sprawdza wartość zwracaną. Powinieneś otrzymać błąd dotyczący „brakującej instrukcji return”, jeśli kontrola może spaść z końca funkcji innej niż void.
cHao
Chodzi mi o to, że int result = -n; if (n < 1) { result = 0 } return result;jeśli kodujesz, gdy kompiluje się dobrze, a kompilator nie ma pojęcia, czy zamierzasz uczynić go równoważnym funkcji z mojego przykładu. Może ten przykład jest zbyt prosty, aby technika wyglądała na pomocną, ale w funkcji z wieloma gałęziami fajnie jest wyjaśnić, że wynik jest przypisywany dokładnie jeden raz, niezależnie od tego, którą ścieżką podążamy.
minopret
Jeśli jednak powiesz if (n < 1) return 0; else return -n;, że skończysz bez problemu ... a poza tym jest to prostsze. :) Wydaje mi się, że w tym przypadku, „jeden zwrot” reguła rzeczywiście pomaga przyczyną emisji nie wiedząc, gdy wartość zwrócona została ustalona; w przeciwnym razie możesz po prostu ją zwrócić, a Java byłaby w stanie określić, kiedy inne ścieżki mogą nie zwracać wartości, ponieważ nie będziesz już oddzielać obliczania wartości od faktycznego jej zwrotu.
cHao
Albo na przykład swoją odpowiedź, w if (n < 0) return -n; else if (n == 0) return 0; else return n - 1;.
cHao
Właśnie zdecydowałem, że nie chcę spędzać więcej chwil życia w obronie reguły OnlyOneReturn w Javie. To wychodzi. Kiedy i jeśli pomyślę o praktyce kodowania Java, którą mam ochotę bronić, na którą wpływ mają praktyki programowania funkcjonalnego, wymienię ten przykład. Do tego czasu nie ma przykładu.
minopret
0

Najłatwiejszym sposobem, aby się tego dowiedzieć, byłoby przesłanie następujących informacji do kompilatora Frege i sprawdzenie wygenerowanego kodu Java:

module Main where

result = take 25 (map sqr [1..]) where sqr x = x*x
Ingo
źródło
Po kilku dniach moje myśli wróciły do ​​tej odpowiedzi. W końcu moja część sugestii dotyczyła implementacji funkcjonalnych części programowania w Scali. Jeśli rozważymy zastosowanie Scali w tych miejscach, w których naprawdę myśleliśmy o Haskell (i myślę, że nie jestem jedynym blog.zlemma.com/2013/02/20/… ), czy nie powinniśmy przynajmniej rozważyć Frege?
minopret
@minopret To naprawdę niszowa tragiczna sytuacja Frege - ludzie, którzy poznali i pokochali Haskella, a mimo to potrzebują JVM. Jestem pewien, że pewnego dnia Frege będzie wystarczająco dojrzały, aby przynajmniej poważnie się zastanowić.
Ingo