Hacker News new | past | comments | ask | show | jobs | submit login
Next-gen Java Programming Style (codemonkeyism.com)
99 points by fogus on Aug 10, 2009 | hide | past | favorite | 75 comments



> Do not use loops for list operations.

I can not get behind this in Java if the filter, fold, or map operation is simply a one-off. (It's another matter entirely though if the power of the functional approach is useful in the situation, say if a filter should be switched dynamically.) To do this for something static though, such as eliminating empty strings or adding an array, is just worse than useless.

This is as clear an example as any of programming against the language, not into it. The code is longer, it is slower, and _for a Java programmer_ it is much less clear. Java was explicitly designed not to be a language for showing off cute constructs for their own sake.


The code is longer: Without having something like extension methods this might be the case. I'm a C# dev so I'll partially concede here due to ignorance. That predicate syntax really does suck... it'd be nice if you had lambdas but [shrugs]

The code is slower: It is slower but in my experience it is very rare that our performance bottle necks occur in these sections of code. We have actually profiled our code and changed from for loops in the legacy version to a redesigned version of the class using this more functional approach during refactoring and we can meet our performance needs just fine. More often than not the big performance improvements lie in disk i/o or more intelligent algorithms rather than just a general use of more functional idioms.

Much less clear _for a Java programmer_: Isn't that always the case until programmers start to learn other ways of doing things? If this was just one guy and not what appears to be a industry wide sea-change maybe I wouldn't argue. The functional paradigm however is being given a fair amount of attention and I don't think it's something you can classify as a niche concern.

"cute constructs for their own sake"- I believe the point of developers moving away from loops is that we are starting to realize that explicitly asking the compiler to loop through these items in this particular order is over-specification 99% of the time. Really we are just saying do this to each one of these. By shifting to a functional paradigm these types of tasks can be easily parallelized and abstracted to the point that you really care about.


Maybe it is just me, but at least to me, a loop and an if (and an accumulating list) is far, far, far simpler (and thus, clearer) than... creating a new anonymous subclass from a templated class with an overriden method and invoking some other method? (at least, I think that's what's going on, I am not sure. A bad sign). And this is not because I don't get filter and functional programming (coming from pythons listcomps, having used haskell and lisp)


How much power is wasted worldwide by slow code that isn't the bottleneck? Surely, fast habits are better than slow habits, all else being equal.


How much power is wasted worldwide by misguided programmers doing premature optimization? All else is never equal.


What kind of question is that? Of course fast > slow where all else is equal, the problem is that that isn't the case. I made more than one point regarding the use of FP style where applicable and there's others as well (including maintainability which is a huge money hole).

Whether or not you think the performance implications are worth the benefits is a personal, contextual decision. But you just grossly misrepresented the entire point by saying that both styles are equivalent except that one is slower.


My CPU's idles most of the time.


Another thought:

Why doesn't Java make it easier to refer to a method and call it dynamically?

For this example:

    List<Person> beerDrinkers = new ArrayList<Person>();
    for (Person p: persons) {
        if (p.getAge() > 16) {
	        beerDrinkers.add(p);
        }
    }
If we had a legalAge method defined on Person returning true for any person over 16, then it would be nice to write the equivalent of

    List<Person> beerDrinkers = filter(persons, legalAge);
which would call the legalAge method on each person and keep the ones returning true. Like most things with Java, you can do this, kind of, but it means getting the instance class, looking up the method for the method name and signature, then invoking on the instance, all of which makes it just not worth it. Objective C has the idea of selectors, which is an easy way to specify the message you want to send to an object, which probably comes from some similar concept in SmallTalk.

This is arguably a more object oriented solution to this general problem. For one, it better follows Demeter's Law, as you do not need to expose the getAge property.


In C++, the way you would do this involves boost::men_fn (http://www.boost.org/doc/libs/1_39_0/libs/bind/mem_fn.html):

  list<Person> beerDrinkers = filter(persons, boost::mem_fn(&Person::legalAge));
Where filter looks something like:

  template <class Sequence, class Pred>
  Sequence filter(const Sequence& in, Pred p)
  {
    Sequence out;
    for (typename Sequence::const_iterator i = in.begin(); i != in.end(); ++i) {
      if (p(*i)) {
        out.insert(out.end(), *i);
      }
    }
    return out;
  }
We can even use this style if legalAge is something that takes a parameter, like ageCheck(age) using boost::bind (http://www.boost.org/doc/libs/1_39_0/libs/bind/bind.html):

  list<Person> beerDrinkers = filter(persons, boost::bind(&Person::ageCheck, 21, _1));


It could be worthwhile if this proposal were implemented:

http://docs.google.com/Doc.aspx?id=k73_1ggr36h

"Here is the same code rewritten using the proposed syntax:"

    List<String> ls = ... ;
    Collections.sort(ls,
        Comparator<String>(String s1, String s2){ return s1.length() - s2.length(); });
By making anonymous inner class declaration more concise, you get many of the benefits of closures, without changing Java semantics.


I disagree. The fact that it is slower has no practical impact in the vast majority of cases, and is certainly not nearly as important as readability. So it comes down to which is clearer, and I find the functional style - even with the extra ceremony - more clearly conveys the code's intent. No messing around with the how, just show me the what. It's not about showing off, it's about abstraction of extremely common idioms.

Pointing this out as unclear for a "Java programmer" is a bit of a red herring I think. Anyone can learn what map/filter/reduce/etc mean in no time at all -- it seems a simple task compared to learning the project-specific abstractions of any codebase.


> Pointing this out as unclear for a "Java programmer" is a bit of a red herring I think.

I think it's really not. There's a talk out there, I think by Gosling, about the direction of Java including why lambdas were rejected for Java 7. One reason was actually to prevent the sort of functional code that polarizes people in terms of whether it is seen as beautiful or opaque. (E.g. currying, Y combinators.)

Java provides the flexibility to use functional programming through anonymous inner classes, but at considerable pain to the author. The point people miss is that that pain is actually a conscious part of the language design.

Anyone can learn about map/filter/reduce, and every programmer needs to for the situations when they are crucial. But on the other hand, the problems with explicit looping are much overstated and I'll admit to emotionally siding on the issue with the creators of Java and Python.

I don't think you can ever fully get the "how" out of programming, nor should you try, nor is "what" implicitly superior all the time in all situations.


> The point people miss is that that pain is actually a conscious part of the language design.

That may be true, but it doesn't make it the right choice. And AFAIK Gosling himself has always been pro-closures, to remove this pain (see http://blogs.sun.com/jag/entry/closures).

>I'll admit to emotionally siding on the issue with the creators of Java and Python

The big difference in Python is that there are "pythonic" alternatives for the really common cases.

> nor is "what" implicitly superior all the time in all situations.

I agree there is such a thing as too much abstraction, but I really don't think map/filter/etc fall into this category. These operations are so frequent that if you don't abstract the how, you end up repeating it thousands of times in a significant codebase. At that point I think you end up on the wrong side of the fence.


I agree, but unlike the other commenter, I think some equivalent of LINQ or concise (i.e. using type inference) syntax for lambdas is necessary to really make it work. And even then, for imperative operations that need to be applied to a subset of elements of a list, an imperative for-loop is still usually the best way to do it.


I don't understand google's awful implementations. In my code by contrast, a map function looks like this:

  public double foo(double x)
  {
    return x*x;
  }

  list=Utility.map(this,"foo",list);


You need to do reflection, which probably has performance implications. You also cannot do closures, which you can simulate with anonymous inner classes.

Might be worth the trade off in your case, but maybe not for the cases Google has in mind.


Not only does that require reflection, it is very error prone and not refactor-friendly. Just change the type of the list, change the method, and it will blow up on runtime instead of compile time. In my opinion, that goes against the grain of Java (and c#, in a way) of having the compiler doing a lot of checking for you.

Google's approach, while more verbose, is a bit safer and more idiomatic.


In addition to the other comments, the Utility class used in this example defeats the purpose of OO programming that Java is more in tune with. What's to stop the proliferation of these Util classes with static methods?

I believe the more Java way would be to extend the List interface to add a map method and implement a corresponding class. To make this less of a pain, extend from one of the JDK provided classes if possible... but yes, it's still a pain.


Bizarre advice. Throw away the mutable aspects of Java to try and code in this "new hotness", a pale echo of an OCaml-esque style of programming. In the process you make code that is neither as declarative as functional code, nor elegant as Java code. Stop trying to code like another language and use another language.

That's enough for me to discard this article as chaff, but considering it even further, has anyone stopped to ask how the Java runtime and compiler handles these changes? Making new classes instead of mutating existing ones doesn't strike me as an efficient use of the system.

Finally, I am so sick of people lamenting being "trapped" in Java. This nonsense of being "trapped" is just a shield for people who don't want to go through the hassle of really using a different language. If I can squeak new languages in at Lockheed Martin on a huge spacerange project who's spec was literally conceived when I was still in diapers, then anyone can do it anywhere if they have the will.


The runtime system seems to be fine with this, at least the results for scala code (which compiles to bytecode consisting of many small, mostly immutable classes with shot lived instances) indicate that this style of programming hits a sweet spot for the JIT compiler and the GC (at least for sun's server VM) which has a far easier time inlining things than with code consisting of few big classes with long lived objects.


In some spaces (financial, perhaps military) running the JVM is the only sensible thing to do. It has special status that allows you to skip steps at audit, which allows your customers to install it, it allows you to use libraries and it's really good for cross-platform so long as you don't care about GUIs (and with everyone using web these days, there's no reason to care about GUI).


True, but who's to say you can't use another language in the JVM - eg. JRuby, Jython or Clojure


"Refactoring is easy, should you need it. If you do not control all the code and it’s usage, you might need to be more careful though."

Bloch's Effective Java is wonderful, but if it has a weakness, it might be that it assumes everyone is writing a library to be used by a lot of developers, many of who are outside your organization.

The example of public fields in data transfer objects is an example of this. Making the fields public is fine if this code is just for you and your team, because you can always refactor it to use accessors later if you need to. But if this code is in a library used by people outside your organization, introducing accessors later without breaking code will be almost impossible.


Public fields like that announce that the object is actually an immutable struct with no internal workings. A need to change it to add computed (or worse, mutable) fields is probably a code smell.


This is one programmer's opinions dressed up as a definitive list. We might as well be discussing bracing preferences - always futile without evidence.

For what little it's worth:

1. Minor benefit as noted. Most devs don't do this. Note that in point 3 his sample loop doesn't declare the loop item final so I doubt he does it all that often either.

2. Ok so far as it goes.

3. What objective benefit am I supposed to get here? If we're pulling preferences out of our bums, I'll go for meaningful names in loop variables thanks.

4. Says who? AT&T braces for the win!

5. Not my preference. Fails the YAGNI principle certainly (which he's apparently in favour of in point 2).

6. Who says it's too hard to use? For simple threading problems the existing tools are more than adequate.

7. Again, what benefit does this convey? It's used in a few places in Java (StringBuffer/Builder) but hasn't proven popular more generally. I suspect there are good reasons for that.

8. Not wildly unreasonable, but Getter/Setter pairs often turn out to be necessary in fairly short order.


What makes me laugh is the commenter defending getters and setters as encapsulation.

No, what getters and setters are is a huge verbose door cut through your encapsulation, turning a class into a mere vending machine.


Agreed. Probably his confusion stems from the fact that Java can expose properties directly. Eww.


I have to admit that the 5th point (Use many, many objects with many interfaces) leaves me thinking...

On the one hand I understand where the author wants to go : by implementing many many interfaces you eventually sort of "emulate" duck-typing, while at the same time reinforcing type safety (specially when it comes to String).

On the other hand, it sounds like a good way to make something that was verbose to begin with even more verbose... but maybe I'm mistaking. I don't do much Java recently (some GWT/GXT sometimes), but I will probably give it a practical try...


The problem I see is the amount of code that has to be copy-pasted, due to Java's lack of traits. If you want to write code like this, do yourself a favor and write it in Scala instead.


Java's lack of traits does indeed suck, and I would definitely prefer Scala over Java, but the only copy-pasting you need to do is of delegation methods. It's duplicate boilerplate, but it's not duplication of logic.


Not necessarily verbose, but moreso a larger codebase. Besides -- all of these extra classes are invisible with D.I. anyway right? ;)


You're right, it will grow the codebase more than making the code verbose. And the code will probably be more readable.

But have you actually used this in practice ? Is it tractable ?

I'm actually thinking of trying this on (a rather old version of) C#, since I'm writing quite lots of code for (an old version of) mono. If only I could use F# sigh


I come from a C# background, learning Java, and I just so happen to be following that process. I don't remember where I picked it up (something in the Spring docs?), but it seemed like a good idea.

Short version: yea, it works pretty well.

Long version is at this shameless blog plug (no ads): http://itgoon.blogspot.com/2009/08/javas-lots-of-little-file...


The proliferation of classes should be manageable if you package them neatly.


Although I do agree that you shouldn't reassign a parameter I don't agree with using the final keyword everywhere. Any decent koder knows not to modify a parameter. If you want to enforce something like this then parse the code on check in and refuse the check in if a parameter is modified. Don't bloat the kode with a reactionary workaround.


I program extensively in C++, not Java. I'm in the habit of using the const qualifier wherever appropriate - even if it's a local variable. It's an easy way to tell myself, others and the compiler that what I'm declaring isn't actually a variable, but a named value.

I don't see how using a const or final qualifier where appropriate increases bloat.


So having to write extra code to extend your versioning system to parse whatever language your code is in and then check it to see if you've altered a variable value doesn't increase bloat, but adding the 'final' keyword does?

There are other problems with that idea, the first of which is that if someone else takes your code and extends it (say, for an open source project) they won't have the same "checking system" that you have, and so it won't catch their mistakes (while using "final" would). Also, I'm not a Java programmer (I mostly use C# or C), but marking classes/methods/variables in C# as 'sealed', 'final', 'static', etc. actually increases the running speed of that program on the CLR because it is able to skip some safety checks on the IL code when it is executed; I don't know that the JVM does that, but it might be worth looking into.


Most JVM implementations that I am aware of do sane things with respect to optimisation of immutable values.


I would use final if something is final because it would also help to understand the code. It is a piece of information that something is final.

Don't forget that final things can still be changed, though.

somethingFinal.setName("teehee"); is still possible even if somethingFinal is final.


Sure, but that would be deliberately going against the tips in the article. The idea is to not mutate values, rather, you instantiate new objects to store new values.


"parse the code on check in and refuse the check in if a parameter is modified"

This would be know as really-really-late-binding.


What's so great about "Fluent" interfaces? If a language supports named parameters, they're inappropriate, no?

(Yes, I know Java doesn't have named parameters, although it and many other languages that don't have it have a work-around with assignment. this is a general question.)


Fluent interfaces suck. Mutating objects is not acceptable, especially when you should pass the correct stuff to the constructor.

But this is Java, and it is the only way to cleanly allow inheritable initargs.

(Perl/Moose and CLOS get this very right. I only use setters in those languages in very, very rare occasions, and it is usually because I am hacking around something messy.)


Python is newer than Java? :D


In fact, Python appeared around 1991, while the official Java Programming Language appeared around 1995 ;)


But Python evolves fast! Every 3 years GvR finishes a chapter of SICP and Python gets better.


"The Java concurrency primitives like locks and synchronized have been proven to be too low level and often to hard to use"

synchronized is not hard to use. it's rather not a good practice to use it, since it will slow down code massively.


Concurrency in general is hard. You'll find that most uses of concurrency in production systems is restricted to very specific patterns. This is probably not a conscious decision, rather it's just very likely that straying too far from very simple models of interaction between concurrent processes is actually just beyond the intellectual scope of most people. I do concurrency theory for a living, and concurrency primitives in Java still give me nightmares.


Concurrency is hard only if you use the common 'pthread-like' model. There are much better alternatives like Erlang and CSP ( http://books.cat-v.org/computer-science/csp/ )


Actually synchronized is not that expensive in itself. Especially in modern implementations it's not bad at all. It always used to amuse me how Java developers would go to contortions trying not to synchronize a method and then they'd fill it up with calls to things like Vector (every method synchronized!), StringBuffer (yep, every method synchronized!) and any number of other calls that are synchronized in the default JDK.


I think it's funny that this is right next to the interview with Rich Hickey :)


If you look at the source for Clojure you'll see many (if not all) of these techniques used throughout with an additional focus on static classes and methods.


I guess what I was getting at is that real next gen style uses the JVM, not Java.

Down with Java, long live the JVM


In the meantime, if Java teams start implementing these practices, and more Java developers get comfortable with them, making the leap to Clojure, Scala, etc. won't be such a shock. Those languages will just require less boiler plate to write the kinds of programs they are already writing.


I don't see why people love the JVM if they don't like Java. I agree that it is nice to have lots of libraries for free, but you also have the burden and limitations of living with something that was created specifically for Java. Languages such as Python and Ruby have been much more successful outside the JVM.


They love the JVM because it's everywhere they want their apps to be; ubiquity has its own beauty.


Really good advice. Whenever I encounter Java code that employs some or more of these techniques, I feel a little bit better about the world.


Are you joking? This advice is horrible.

He is advocating taking a verbose language and making it even more verbose and far less efficient. Think about what the final in "public final int x;" means in #8.

Many Java developers have fallen for the idea that everything is an interface to an interface. However, if you start by trying to get stuff done, you find a most scaffolding is pure waste.


He is advocating using a style more like modern functional programming languages such as ML, Haskell and F#.

The example in #8 is essentially recreating primitive tuple types present in the languages I just mentioned. Given that the overheads on such an object are small, and the garbage collector is good at dealing with short-lived objects, it makes sense to instantiate a new class when it is necessary to mutate it. Your optimiser will love you, and maintaining this style of code is considerably easier.


Functional programming is ancient.

You might be all atwitter about the power of functional code. But, it's an old idea. Emulating the functional style a language that's not based on it provides minimal gain, at the cost of code bloat.

PS: Pun was intended to be funny not mean. This really is an old idea, which is occasionally useful, but mostly it's a waste of time, money, and CPU cycles.


Sure, but I used the word modern. As in, "of the modern era". There are old functional programming languages and there are young functional programming languages. The fact is that most of the interesting research into programming languages these days goes ends up in functional languages such as Haskell or an ML-variant.

And I really don't buy your "bloat" claim. Surely mutating member variables unnecessarily is "bloat". And having redundant getters and setters seems pretty bloated to me. In fact, forcing everything in the universe into a paradigm that largely consists of straight-line imperative code boxed up in classes seems like the ultimate example of bloat, given that one could simply code in an imperative style without using objects.

If anything, I would think that the measures suggested would result in marginally smaller code-size, which I assume is the metric you are employing to measure "bloat". In performance terms, I would imagine that these suggestions would result in no performance impact, or some small improvement as it is quite likely that the optimiser will be able to do a better job if you apply this style uniformly. As you say, it might be a minimal gain, but don't we as programmers have something of a responsibility to write the best programs we can and use our tools to the fullest of our abilities, especially when the cost is zero or almost-zero?

So what metric are you using here? I assume it's something a bit fuzzy like "readability" or "ease of maintenance"? In that case, you're basically saying "things that are different to what I already do are harder to do", which in addition to being mildly silly, is a total cop-out and is intellectual dishonesty. Too many good ideas in programming languages have been sunk by programmers who hate the idea of not being the expert all of the time, with such behaviour being hidden behind very weak justifications that sound technical, but are actually just about egos.

Naturally, this might not be the case with you at all! However, I'd be very interested to know on what objective basis you are making judgements about the awfulness and bloatedness of code (in any language) that employs a more "functional" style.


There is a reason that research into functional languages is limited to an old or new functional language. Without syntax and compiler support you don't gain any power to offset the limitations.

Anyway, the power of OOP is creating intelligent data. You can "automatically" do things with a Point that you need to explicitly do with integers. If you start dealing with a Fixed_Point objects, and Mobile_Point object, you now have bloat with little gain. It's far better to have a "Final" flag on your point, so it will toss an error when you try to change it vs. trying to maintain 2 separate classes. That way if you suddenly need to change your point you have to refractor less code.

Granted, if Java's foreach would automatically make helper threads to speed things up then there would be an advantage. But, changing the form without compiler support is pointless.

PS: Feel free to test your ideas, but allocating memory is still the enemy of speed. When you create a temporary object outside of the stack it really does slow things down.


I am glad to see someone else equally irritated by this "fashion statement" that people engaging in, abusing their languages to ape other languages. Even worse, most don't really understand the benefits of the languages they copy, they pull the iconic cases and then pretend that languages aren't a teepee of features that cannot stand when just a few poles are pulled in isolation.


Huh? Are you suggesting that people use functional programming languages such as Haskell or ML because they are fashionable? Where on earth are you living such that they are fashionable? I wanna live there.


Please reread what I said. The fashion statement is using a non-functional language and adopting some functional mores.


I see. I'm still not sure how you justify your (revised) statement. What's fashionable about it? You don't think people do stuff because they think it makes them better programmers, rather than because of some perceived fashion trend?

Similarly, you don't think the tendency for most non-functional languages to adopt features and techniques from functional languages is because those ideas are inherently valuable? Sometimes a good idea is a good idea, and I think functional programming is an idea whose time has finally come. Maybe what you're labeling "fashion" is actually just a natural progression towards acceptance and adoption that started several decades ago.

Personally I think the trend towards selectively adopting functional features (if there is one!) could only be positive. If more programmers become comfortable with the concepts underpinning FP, then making the jump to using plain ol' FP (rather than <insert your language here> with anonymous first-class functions bolted on etc) will likely be much easier. Maybe it'll lead to wide-spread adoption!


I did not revise my statement. You simply read it incorrectly.

To directly answer your question, I think that the merit of functional programming techniques is in their application as a whole. Sniping high-profile individual techniques does not bring a proportional benefit. It is simply cargo culting. It is "fashionable" to mention "functional" programming and to butcher the concept of referential transparency when writing code in Java, C++ and C# these days. All it does is create a new style of obfuscation for these languages.


> There is a reason that research into functional languages is limited to an old or new functional language. Without syntax and compiler support you don't gain any power to offset the limitations

I don't understand what this means.

> If you start dealing with a Fixed_Point objects, and Mobile_Point object, you now have bloat with little gain

Why would you not have a single point class implementing multiple interfaces? Surely if they do the same thing, you wouldn't bother. Good design would dictate that if they do similar things (but not the same), they should probably share an abstract parent class anyway.

> It's far better to have a "Final" flag on your point, so it will toss an error when you try to change it

And check this at runtime for every access!?! Why on earth would that be more efficient than static checking at compile time?


I don't understand what this means.

EX: Tail recursion (http://en.wikipedia.org/wiki/Tail_recursion) will be optimization if your code is a specific form AND your compiler also supports transforming to into loop. However, if you use the form without compiler support you are killing the stack and should manually convert your code to a loop by hand. Edit: Tail recursion really is just another loop; it's often used as a backhanded way to have mutable variables in a functional language, but in Java you might as well just have a loop.

And check this at runtime for every access!?! Why on earth would that be more efficient than static checking at compile time?

You only need to check it when you are updating a member variable not when you read it. Note: it does not save you from updating the variables inside the class if you bypass the getters and setters, but that’s a side issue. Anyway, if you check it ~50 times vs. creating one extra object you should come out ahead in speed. More importantly you will have far less and more maintainable code.

PS: Using multiple interfaces to the same class is a reasonable option, with some tradeoffs. You still need to maintain more code, but it is pure fluff so not that big a deal. However, that's not what he did.


Do you have any figures to back up the claim that memory allocation for small, short-lived objects consisting of final members is actually as slow as you claim? My understanding is that it isn't.


That's not a simple question. A name with string first, string last takes longer to create than a point with int x, int y.

Temporary fixed size objects can be created and destroyed with little overhead. But you need to profile things to get a good metric. http://java.sun.com/j2se/reference/whitepapers/memorymanagem... Has some of the details.

Creating an object that lives on the processor cache can take 150 cycles which is FAST. But, the more complex the object the longer the allocation, and you can quickly start to fragment your memory if you have several short lived objects that don't die at the same time. Assuming your checking a variable in L1 cache then it's still an order of magnitude faster, but once you start taking longer to load the object into cache etc so it's not really a stable time either...

PS: It's going to get really application specific so I can't think of realistic test outside of an actual application. Still I will try some simple cases tomorrow.


final ... ah yes, the old final.

Just remember that final is a signal to the compiler that it can perform certain optimizations on your code. This includes inlining the value.

So if you use

  public static final KEY="value1";
you can get inconsistent class files if you change the value, the copy classes that use KEY 'variable' across selectively.

This is most ant webapp and J2EE compiling workflows.


That sounds like really broken compiler or build script behaviour, rather than something intrinsically wrong with the use of final.


  When the compiler sees a final method call it can 
  (at its discretion) skip the normal approach of inserting code 
http://www.codeguru.com/java/tij/tij0071.shtml


A final method is different from a final variable




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: