FizzBuzz bez ifów w javie

No dobra… po prostu będzie to przykład wykorzystania popularnych narzędzi by stworzyć FizzBuzz bez pisania ifów. W ogólności problem rozbija się o dwie brakujące metody. Pierwsza dostarczająca „zoraclowanego” String-a, czyli takiego gdzie pusty String będzie oznaczać null. Druga to cykl. Pierwszą metodę weźmiemy z Apache Commons Lang, a drugą z Guavy. Cały pomysł bazuje na ostatniej JUG-owej prezentacji Pawła Szulca, który pokazał to coś w Haskell-u. Mi się to spodobało i poniżej wersja w javie.

Listing 1. FizzBuzz bez ifów

public class FizzBuzz {

	public static void main(String[] args) {
		Iterator<String> fizz = Iterables.cycle("", "", "Fizz").iterator();
		Iterator<String> buzz = Iterables.cycle("", "", "", "", "Buzz").iterator();

		OfInt ints = IntStream.iterate(1, n -> n + 1).iterator();
		Iterator<Optional<String>> strings = Stream.generate(() -> StringOptional.of(fizz.next() + buzz.next()))
				.iterator();

		Stream.generate(() -> {
			Optional<String> s = strings.next();
			Integer i = ints.next();
			return s.orElse(i.toString());
		}).limit(32).forEach(System.out::println);

	}
}

class StringOptional {

	public static Optional<String> of(String v) {
		return ofNullable(defaultIfEmpty(v, null));
	}
}

Na początek klasa StringOptional, która będzie nam zamykać wywołanie StringUtils.defaultIfEmpty tak by ładnie produkowało Optional.

I teraz metoda main. Na początek tworzymy dwa cykle pierwszy reprezentuje częstość dla fizz, drugi dla buzz. W przypadku uogólnionym można pokusić się o zaimplementowanie tego w oparciu o metodę w rodzaju Collections.fill i listy o określonej długości. Przy czym należy pamiętać, że java jest ewaluowana zachłannie i dla dużych wartości całość się wywali.

Następnie tworzymy dwa iteratory z nieskończonych Stream-ów. Pierwszy generuje kolejne inty, a drugi w oparciu o konkatenację wartości z cykli oraz naszą magiczną klasę StringOptional odpowiednią wersję Optional.

Ostatnia linijka składa nam to wszystko do kupy. Tworzymy Stream, który będzie budowany według logiki jeżeli optional.empty to wypisz numer inaczej wypisz zawartość. Przy czym warunek jest weryfikowany na poziomie typu Optional, a ten jest determinowany w metodzie ofNullable, zatem pochodzi „z języka”.

Co autor miał na myśli?

Powyższy kod to przykład na to jak odpowiednie użycie typów pozwala na uproszczenie algorytmu. Dobór poszczególnych elementów na początku pozwolił na uproszczenie algorytmu. Co więcej kod ten można zrealizować jeszcze prościej:

Listing 2. FizzBuzz uproszczony

public class FizzBuzz {

	public static void main(String[] args) {
		Iterator<String> fizzBuzz =
				cycle("", "", "Fizz", "", "Buzz", "Fizz", "", "", "Fizz", "Buzz", "", "Fizz", "", "", "FizzBuzz")
						.iterator();

		OfInt ints = iterate(1, n -> n + 1).iterator();
		Iterator<Optional<String>> strings = generate(() -> ofNullable(defaultIfEmpty(fizzBuzz.next(), null)))
				.iterator();

		generate(() -> strings.next().orElse(ints.next().toString()))
				.limit(32).forEach(System.out::println);

	}
}

Ale w tym przypadku pierwsza linijka z fizzBuzz wprowadza trochę zagmatwania ponieważ specyfikacja którą reprezentuje jest tak naprawdę „dwuwymiarowa” tzn. ograniczona przez dwa warunki. W tej wersji one „giną” w postaci jednego dość rozwlekłego warunku.

Odpowiednio dobrane typy pozwalają na specyfikowanie algorytmu w oparciu o nie właśnie, a nie jakieś arbitralne zewnętrzne warunki. W dodatku pozwalają na lepsze przetestowanie, bo testujemy implementację typu, oraz dają większe bezpieczeństwo końcowego rozwiązania ponieważ aplikujemy funkcje, których dziedzina i przeciwdziedzina są ograniczone do konkretnych typów (a mapowanie A→B zazwyczaj może być realizowane w jeden konkretny sposób).

Jedna myśl na temat “FizzBuzz bez ifów w javie

  1. Wersja bez zewn. bibliotek. To co w artykule ukryte jest w Cycle, tutaj robi jawnie operator modulo.

    Map map = new HashMap();
    map.put(3, „Fizz”);
    map.put(6, „Fizz”);
    map.put(9, „Fizz”);
    map.put(12, „Fizz”);
    map.put(5, „Buzz”);
    map.put(10, „Buzz”);
    map.put(0, „FizzBuzz”);

    IntStream.iterate(1, n -> n + 1)
    .mapToObj(i -> map.getOrDefault(i % 15, String.valueOf(i)))
    .limit(32)
    .forEach(System.out::println);

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax