Zemsta kujonów
Chcesz założyć startup? Uzyskaj finansowanie od Y Combinator.
Maj 2002
| "Ścigaliśmy programistów C++. Udało nam się przyciągnąć wielu z nich o połowę drogi do Lispa."
- Guy Steele, współautor specyfikacji Javy
W branży oprogramowania trwa nieustanna walka między zakutymi łbami z akademii a równie potężną siłą, jaką są szefowie z zakutymi głowami. Każdy wie, kim jest szef z zakutą głową, prawda? Myślę, że większość ludzi w świecie technologii nie tylko rozpoznaje tę kreskówkową postać, ale także zna prawdziwą osobę w swojej firmie, na której została ona wzorowana.
Szef z zakutą głową cudownie łączy dwie cechy, które same w sobie są powszechne, ale rzadko występują razem: (a) nie wie absolutnie nic o technologii i (b) ma o niej bardzo mocne opinie.
Załóżmy na przykład, że musisz napisać fragment oprogramowania. Szef z zakutą głową nie ma pojęcia, jak to oprogramowanie powinno działać, i nie potrafi odróżnić jednego języka programowania od drugiego, a jednak wie, w jakim języku powinieneś je napisać. Dokładnie. Uważa, że powinieneś napisać je w Javie.
Dlaczego tak myśli? Przyjrzyjmy się mózgowi szefa z zakutą głową. Myśli on mniej więcej tak: Java jest standardem. Musi być, bo ciągle czytam o niej w prasie. Skoro jest standardem, nie będę miał kłopotów z jej używaniem. A to oznacza również, że zawsze będzie wielu programistów Javy, więc jeśli programiści pracujący dla mnie teraz odejdą, co programiści pracujący dla mnie tajemniczo zawsze robią, będę mógł ich łatwo zastąpić.
Cóż, to nie brzmi tak nierozsądnie. Ale wszystko opiera się na jednym, niewypowiedzianym założeniu, a to założenie okazuje się fałszywe. Szef z zakutą głową wierzy, że wszystkie języki programowania są w zasadzie równoważne. Gdyby tak było, miałby rację. Jeśli języki są równoważne, to jasne, używaj tego, który używa każdy inny.
Ale wszystkie języki nie są równoważne, i myślę, że mogę to udowodnić, nawet nie zagłębiając się w różnice między nimi. Gdybyś zapytał szefa z zakutą głową w 1992 roku, w jakim języku powinno być pisane oprogramowanie, odpowiedziałby z takim samym brakiem wahania, jak dzisiaj. Oprogramowanie powinno być pisane w C++. Ale jeśli języki są równoważne, dlaczego opinia szefa z zakutą głową miałaby się kiedykolwiek zmienić? Właściwie, dlaczego twórcy Javy w ogóle fatygowali się tworzeniem nowego języka?
Przypuszczalnie, jeśli tworzysz nowy język, to dlatego, że uważasz go za lepszy w jakiś sposób od tego, co ludzie już mieli. I faktycznie, Gosling jasno stwierdza w pierwszym białym papierze Javy, że Java została zaprojektowana, aby naprawić niektóre problemy z C++. Więc masz to: języki nie są równoważne. Jeśli podążysz tropem przez mózg szefa z zakutą głową do Javy, a następnie z powrotem przez historię Javy do jej początków, znajdziesz się z ideą, która zaprzecza założeniu, od którego zacząłeś.
Więc kto ma rację? James Gosling czy szef z zakutą głową? Nic dziwnego, że Gosling ma rację. Niektóre języki są lepsze, dla pewnych problemów, niż inne. I wiesz, to rodzi kilka interesujących pytań. Java została zaprojektowana, aby być lepsza, dla pewnych problemów, niż C++. Jakie problemy? Kiedy Java jest lepsza, a kiedy C++? Czy są sytuacje, w których inne języki są lepsze od któregokolwiek z nich?
Kiedy zaczniesz rozważać to pytanie, otworzysz prawdziwą puszkę Pandory. Gdyby szef z zakutą głową musiał myśleć o problemie w całej jego złożoności, jego mózg by eksplodował. Dopóki uważa wszystkie języki za równoważne, wszystko, co musi zrobić, to wybrać ten, który wydaje się mieć największą dynamikę, a ponieważ jest to bardziej kwestia mody niż technologii, nawet on prawdopodobnie uzyska właściwą odpowiedź. Ale jeśli języki się różnią, nagle musi rozwiązać dwa równania jednocześnie, próbując znaleźć optymalną równowagę między dwiema rzeczami, o których nic nie wie: względną przydatnością około dwudziestu wiodących języków do problemu, który musi rozwiązać, oraz szansami na znalezienie programistów, bibliotek itp. dla każdego z nich. Jeśli to jest to, co znajduje się po drugiej stronie drzwi, nic dziwnego, że szef z zakutą głową nie chce ich otwierać.
Niedogodnością wierzenia, że wszystkie języki programowania są równoważne, jest to, że to nieprawda. Ale zaletą jest to, że znacznie upraszcza to życie. I myślę, że to główny powód, dla którego ta idea jest tak rozpowszechniona. To wygodna idea.
Wiemy, że Java musi być całkiem dobra, bo to modny, nowy język programowania. Albo nie? Jeśli spojrzysz na świat języków programowania z dystansu, wygląda to tak, jakby Java była najnowszą rzeczą. (Z wystarczająco daleka widzisz tylko dużą, migającą tablicę reklamową opłaconą przez Sun.) Ale jeśli spojrzysz na ten świat z bliska, odkryjesz, że istnieją stopnie „fajności”. W subkulturze hakerów istnieje inny język o nazwie Perl, który jest uważany za znacznie fajniejszy niż Java. Slashdot, na przykład, jest generowany przez Perla. Nie sądzę, żeby ci goście używali Java Server Pages. Ale istnieje inny, nowszy język, zwany Pythonem, którego użytkownicy mają tendencję patrzeć z góry na Perla, a więcej czeka w zanadrzu.
Jeśli spojrzysz na te języki w kolejności: Java, Perl, Python, zauważysz interesujący wzór. Przynajmniej zauważysz ten wzór, jeśli jesteś hakerem Lispa. Każdy z nich jest coraz bardziej podobny do Lispa. Python kopiuje nawet cechy, które wielu hakerów Lispa uważa za błędy. Można tłumaczyć proste programy Lisp na Pythona linia po linii. Jest rok 2002, a języki programowania prawie dogoniły 1958 rok.
Doganianie matematyki
Mam na myśli to, że Lisp został po raz pierwszy odkryty przez Johna McCarthy'ego w 1958 roku, a popularne języki programowania dopiero teraz doganiają idee, które wtedy opracował.
Teraz, jak to możliwe? Czyż technologia komputerowa nie zmienia się bardzo szybko? Mam na myśli, że w 1958 roku komputery były wielkimi potworami wielkości lodówki o mocy obliczeniowej lodówki. Jak jakakolwiek tak stara technologia mogłaby być w ogóle istotna, nie mówiąc już o przewyższaniu najnowszych osiągnięć?
Powiem ci, jak. To dlatego, że Lisp nie był naprawdę zaprojektowany jako język programowania, przynajmniej nie w sensie, w jakim rozumiemy go dzisiaj. Kiedy mówimy o języku programowania, mamy na myśli coś, czego używamy, aby powiedzieć komputerowi, co ma robić. McCarthy ostatecznie zamierzał opracować język programowania w tym sensie, ale Lisp, który faktycznie otrzymaliśmy, opierał się na czymś osobnym, co zrobił jako teoretyczne ćwiczenie -- wysiłek mający na celu zdefiniowanie wygodniejszej alternatywy dla maszyny Turinga. Jak sam McCarthy powiedział później:
Innym sposobem pokazania, że Lisp był zgrabniejszy niż maszyny Turinga, było napisanie uniwersalnej funkcji Lispa i pokazanie, że jest ona krótsza i bardziej zrozumiała niż opis uniwersalnej maszyny Turinga. Była to funkcja Lispa eval... , która oblicza wartość wyrażenia Lispa.... Pisanie eval wymagało wymyślenia notacji reprezentującej funkcje Lispa jako dane Lispa, a taka notacja została opracowana na potrzeby artykułu bez myśli o tym, że zostanie użyta do praktycznego wyrażania programów Lispa.
Następnie, pod koniec 1958 roku, Steve Russell, jeden ze studentów McCarthy'ego, spojrzał na tę definicję eval i zdał sobie sprawę, że jeśli przetłumaczy ją na język maszynowy, wynikiem będzie interpreter Lispa.
To było wówczas wielkim zaskoczeniem. Oto co McCarthy powiedział o tym później w wywiadzie:
Steve Russell powiedział: „Słuchaj, dlaczego nie zaprogramuję tego eval...”, a ja mu powiedziałem: „Ho, ho, mylisz teorię z praktyką, to eval jest przeznaczone do czytania, a nie do obliczeń”. Ale on poszedł dalej i zrobił to. To znaczy, skompilował eval z mojego artykułu do kodu maszynowego [IBM] 704, poprawiając błędy, a następnie reklamował to jako interpreter Lispa, którym z pewnością był. Tak więc w tym momencie Lisp miał zasadniczo formę, jaką ma dzisiaj....
Nagle, w ciągu kilku tygodni, jak sądzę, McCarthy odkrył, że jego teoretyczne ćwiczenie przekształciło się w rzeczywisty język programowania – i to potężniejszy, niż zamierzał.
Tak więc krótkie wyjaśnienie, dlaczego ten język z lat 50. nie jest przestarzały, jest takie, że nie była to technologia, ale matematyka, a matematyka się nie starzeje. Właściwym porównaniem dla Lispa nie jest sprzęt z lat 50., ale powiedzmy algorytm Quicksort, który został odkryty w 1960 roku i nadal jest najszybszym algorytmem sortowania ogólnego przeznaczenia.
Istnieje jeszcze jeden język z lat 50., który przetrwał – Fortran – i reprezentuje on przeciwne podejście do projektowania języków. Lisp był dziełem teoretycznym, które niespodziewanie stało się językiem programowania. Fortran został opracowany celowo jako język programowania, ale w tym, co dziś uznalibyśmy za bardzo niski poziom.
Fortran I, język opracowany w 1956 roku, był zupełnie innym zwierzęciem niż obecny Fortran. Fortran I był w zasadzie językiem asemblera z matematyką. Pod pewnymi względami był mniej potężny niż nowsze języki asemblera; na przykład nie było podprogramów, tylko skoki. Obecny Fortran jest teraz prawdopodobnie bliższy Lispowi niż Fortranowi I.
Lisp i Fortran były pniem dwóch oddzielnych drzew ewolucyjnych, jednego zakorzenionego w matematyce, a drugiego w architekturze maszyn. Te dwa drzewa zbiegają się od tamtej pory. Lisp zaczął jako potężny, a przez następne dwadzieścia lat stał się szybki. Tak zwane języki głównego nurtu zaczęły jako szybkie, a przez następne czterdzieści lat stopniowo stawały się potężniejsze, aż do teraz, gdy najbardziej zaawansowane z nich są dość bliskie Lispowi. Blisko, ale nadal czegoś im brakuje....
Co sprawiło, że Lisp był inny
Kiedy został po raz pierwszy opracowany, Lisp zawierał dziewięć nowych idei. Niektóre z nich dziś uważamy za oczywiste, inne występują tylko w bardziej zaawansowanych językach, a dwie są nadal unikalne dla Lispa. Dziewięć idei, w kolejności ich przyjęcia przez główny nurt, to:
-
Instrukcje warunkowe. Instrukcja warunkowa to konstrukcja typu „jeśli-wtedy-inaczej”. Dziś uważamy je za coś oczywistego, ale Fortran I ich nie miał. Miał tylko warunkowy goto ściśle oparty na podstawowej instrukcji maszynowej.
-
Typ funkcyjny. W Lispie funkcje są typem danych, podobnie jak liczby całkowite czy ciągi znaków. Mają one reprezentację literałową, mogą być przechowywane w zmiennych, przekazywane jako argumenty i tak dalej.
-
Rekurencja. Lisp był pierwszym językiem programowania, który ją obsługiwał.
-
Typowanie dynamiczne. W Lispie wszystkie zmienne są efektywnie wskaźnikami. To wartości mają typy, a nie zmienne, a przypisywanie lub wiązanie zmiennych oznacza kopiowanie wskaźników, a nie tego, na co wskazują.
-
Zbieranie śmieci.
-
Programy składające się z wyrażeń. Programy Lisp to drzewa wyrażeń, z których każde zwraca wartość. Jest to sprzeczne z Fortranem i większością późniejszych języków, które rozróżniają wyrażenia i instrukcje.
To rozróżnienie było naturalne w Fortranie I, ponieważ nie można było zagnieżdżać instrukcji. A ponieważ do działania matematyki potrzebne były wyrażenia, nie było sensu, aby cokolwiek innego zwracało wartość, ponieważ nic nie czekało na nią.
To ograniczenie zniknęło wraz z pojawieniem się języków blokowych, ale wtedy było już za późno. Rozróżnienie między wyrażeniami a instrukcjami zostało utrwalone. Rozprzestrzeniło się z Fortranu na Algol, a następnie na obu ich potomków.
-
Typ symboliczny. Symbole są efektywnie wskaźnikami do ciągów znaków przechowywanych w tablicy mieszającej. Można więc testować równość, porównując wskaźnik, zamiast porównywać każdy znak.
-
Notacja dla kodu używająca drzew symboli i stałych.
-
Cały język jest dostępny przez cały czas. Nie ma prawdziwego rozróżnienia między czasem odczytu, czasem kompilacji a czasem wykonania. Możesz kompilować lub uruchamiać kod podczas odczytu, odczytywać lub uruchamiać kod podczas kompilacji, a także odczytywać lub kompilować kod w czasie wykonania.
Uruchamianie kodu w czasie odczytu pozwala użytkownikom przeprogramować składnię Lispa; uruchamianie kodu w czasie kompilacji jest podstawą makr; kompilacja w czasie wykonania jest podstawą wykorzystania Lispa jako języka rozszerzeń w programach takich jak Emacs; a odczyt w czasie wykonania umożliwia programom komunikację za pomocą s-wyrażeń, pomysłu niedawno wynalezionego na nowo jako XML.
Kiedy Lisp się pojawił, te idee były dalekie od zwykłej praktyki programistycznej, która była w dużej mierze dyktowana przez dostępny sprzęt pod koniec lat 50. Z czasem domyślny język, ucieleśniony w kolejnych popularnych językach, stopniowo ewoluował w kierunku Lispa. Idee 1-5 są teraz powszechne. Numer 6 zaczyna pojawiać się w głównym nurcie. Python ma formę 7, chociaż wydaje się, że nie ma na to składni.
Jeśli chodzi o numer 8, może to być najciekawszy z nich. Idee 8 i 9 stały się częścią Lispa przez przypadek, ponieważ Steve Russell zaimplementował coś, czego McCarthy nigdy nie zamierzał implementować. A jednak te idee okazują się odpowiedzialne zarówno za dziwny wygląd Lispa, jak i za jego najbardziej charakterystyczne cechy. Lisp wygląda dziwnie nie tyle z powodu dziwnej składni, co dlatego, że nie ma składni; wyrażasz programy bezpośrednio w drzewach parsowania, które są budowane w tle, gdy inne języki są parsowane, a te drzewa są zbudowane z list, które są strukturami danych Lispa.
Wyrażanie języka w jego własnych strukturach danych okazuje się bardzo potężną cechą. Idee 8 i 9 razem oznaczają, że możesz pisać programy, które piszą programy. To może brzmieć jak dziwny pomysł, ale w Lispie jest to codzienność. Najczęstszym sposobem jest użycie czegoś, co nazywa się makro.
Termin „makro” w Lispie nie oznacza tego, co w innych językach. Makro Lisp może być wszystkim, od skrótu po kompilator nowego języka. Jeśli chcesz naprawdę zrozumieć Lisp, lub po prostu poszerzyć swoje horyzonty programistyczne, powinieneś dowiedzieć się więcej o makrach.
Makra (w sensie Lispa) są nadal, o ile wiem, unikalne dla Lispa. Jest to częściowo dlatego, że aby mieć makra, prawdopodobnie musisz sprawić, by twój język wyglądał tak dziwnie jak Lisp. Może to być również dlatego, że jeśli dodasz ten ostatni przyrost mocy, nie możesz już twierdzić, że wymyśliłeś nowy język, ale tylko nowy dialekt Lispa.
Wspominam o tym głównie w żartach, ale to prawda. Jeśli zdefiniujesz język, który ma car, cdr, cons, quote, cond, atom, eq i notację dla funkcji wyrażonych jako listy, możesz zbudować resztę Lispa z tego. To jest w zasadzie cecha definiująca Lisp: McCarthy nadał Lispowi taki kształt, aby to właśnie osiągnąć.
Gdzie języki mają znaczenie
Załóżmy więc, że Lisp reprezentuje pewien rodzaj granicy, do której języki głównego nurtu zbliżają się asymptotycznie – czy to oznacza, że powinieneś faktycznie używać go do pisania oprogramowania? Ile tracisz, używając mniej potężnego języka? Czy nie mądrzej jest czasem nie być na samym krańcu innowacji? A czy popularność nie jest w pewnym stopniu sama w sobie uzasadnieniem? Czy szef z zakutą głową ma rację, na przykład, chcąc używać języka, dla którego może łatwo zatrudnić programistów?
Istnieją oczywiście projekty, w których wybór języka programowania nie ma większego znaczenia. Z reguły, im bardziej wymagająca aplikacja, tym większą korzyść uzyskujesz z używania potężnego języka. Ale wiele projektów wcale nie jest wymagających. Większość programowania prawdopodobnie polega na pisaniu małych programów łączących, a do małych programów łączących możesz użyć dowolnego języka, z którym już się znasz i który ma dobre biblioteki do tego, czego potrzebujesz. Jeśli po prostu musisz przesyłać dane z jednej aplikacji Windows do drugiej, oczywiście użyj Visual Basic.
Możesz pisać małe programy łączące również w Lispie (używam go jako kalkulatora stacjonarnego), ale największą wygraną dla języków takich jak Lisp jest druga strona spektrum, gdzie musisz pisać złożone programy, aby rozwiązywać trudne problemy w obliczu ostrej konkurencji. Dobrym przykładem jest program do wyszukiwania cen biletów lotniczych ITA Software, który licencjonuje Orbitz. Ci ludzie weszli na rynek zdominowany już przez dwóch dużych, ugruntowanych konkurentów, Travelocity i Expedia, i wydają się po prostu ich upokorzyć technologicznie.
Rdzeniem aplikacji ITA jest program Common Lisp o długości 200 000 linii, który przeszukuje wiele rzędów wielkości więcej możliwości niż ich konkurenci, którzy najwyraźniej nadal używają technik programowania z epoki mainframe. (Chociaż ITA w pewnym sensie również używa języka programowania z epoki mainframe.) Nigdy nie widziałem żadnego kodu ITA, ale według jednego z ich czołowych hakerów używają oni wielu makr, i nie jestem zaskoczony, słysząc to.
Siły dośrodkowe
Nie mówię, że używanie mniej popularnych technologii nie wiąże się z żadnymi kosztami. Szef z zakutą głową nie myli się całkowicie, martwiąc się o to. Ale ponieważ nie rozumie ryzyka, ma tendencję do jego powiększania.
Myślę o trzech problemach, które mogą wyniknąć z używania mniej popularnych języków. Twoje programy mogą nie działać dobrze z programami napisanymi w innych językach. Możesz mieć mniej dostępnych bibliotek. I możesz mieć problemy z zatrudnieniem programistów.
Jak bardzo jest to problem? Znaczenie pierwszego zależy od tego, czy masz kontrolę nad całym systemem. Jeśli piszesz oprogramowanie, które musi działać na maszynie zdalnego użytkownika na szczycie wadliwego, zamkniętego systemu operacyjnego (nie wymieniam nazw), mogą istnieć zalety pisania aplikacji w tym samym języku co system operacyjny. Ale jeśli kontrolujesz cały system i masz kod źródłowy wszystkich części, tak jak przypuszczalnie ITA, możesz używać dowolnych języków. Jeśli pojawi się jakakolwiek niezgodność, możesz ją naprawić sam.
W aplikacjach serwerowych możesz sobie pozwolić na używanie najbardziej zaawansowanych technologii, i myślę, że to jest główna przyczyna tego, co Jonathan Erickson nazywa "renesansem języków programowania". Dlatego właśnie słyszymy o nowych językach, takich jak Perl i Python. Nie słyszymy o tych językach, ponieważ ludzie używają ich do pisania aplikacji Windows, ale dlatego, że ludzie używają ich na serwerach. A ponieważ oprogramowanie przesuwa się z pulpitu na serwery (przyszłość, na którą nawet Microsoft wydaje się zrezygnowany), będzie coraz mniejsza presja na używanie technologii środka drogi.
Jeśli chodzi o biblioteki, ich znaczenie również zależy od aplikacji. W przypadku mniej wymagających problemów dostępność bibliotek może przewyższyć wewnętrzną moc języka. Gdzie jest punkt przełamania? Trudno powiedzieć dokładnie, ale gdziekolwiek jest, jest on poniżej czegokolwiek, co można by nazwać aplikacją. Jeśli firma uważa się za działającą w branży oprogramowania i pisze aplikację, która będzie jednym z jej produktów, to prawdopodobnie będzie ona obejmować kilku hakerów i zajmie co najmniej sześć miesięcy pisania. W projekcie o takim rozmiarze potężne języki prawdopodobnie zaczną przeważać nad wygodą istniejących bibliotek.
Trzecia obawa szefa z zakutą głową, trudność w zatrudnianiu programistów, moim zdaniem jest czerwonym śledziem. Ilu hakerów musisz zatrudnić, w końcu? Z pewnością do tej pory wszyscy wiemy, że oprogramowanie najlepiej rozwija się w zespołach liczących mniej niż dziesięć osób. I nie powinieneś mieć problemów z zatrudnieniem hakerów na taką skalę dla jakiegokolwiek języka, o którym kiedykolwiek słyszano. Jeśli nie możesz znaleźć dziesięciu hakerów Lispa, to twoja firma prawdopodobnie znajduje się w niewłaściwym mieście do rozwijania oprogramowania.
W rzeczywistości wybór potężniejszego języka prawdopodobnie zmniejsza rozmiar potrzebnego zespołu, ponieważ (a) jeśli użyjesz potężniejszego języka, prawdopodobnie nie będziesz potrzebować tylu hakerów, a (b) hakerzy pracujący w bardziej zaawansowanych językach są prawdopodobnie mądrzejsi.
Nie mówię, że nie będziesz odczuwać dużej presji, aby używać tego, co jest postrzegane jako „standardowe” technologie. W Viaweb (obecnie Yahoo Store) wzbudziliśmy pewne zdziwienie wśród VC i potencjalnych nabywców, używając Lispa. Ale wzbudziliśmy też zdziwienie, używając zwykłych komputerów z procesorami Intel jako serwerów zamiast serwerów „przemysłowych” jak Suny, ignorując rzekomy standard e-commerce o nazwie SET, o którym nikt już nawet nie pamięta, i tak dalej.
Nie możesz pozwolić, aby „garniturki” podejmowały za ciebie decyzje techniczne. Czy to zaniepokoiło niektórych potencjalnych nabywców, że użyliśmy Lispa? Niektórzy, nieznacznie, ale gdybyśmy nie użyli Lispa, nie bylibyśmy w stanie napisać oprogramowania, które sprawiło, że chcieli nas kupić. To, co im się wydawało anomalią, w rzeczywistości było przyczyną i skutkiem.
Jeśli zakładasz startup, nie projektuj swojego produktu tak, aby zadowolić VC lub potencjalnych nabywców. Zaprojektuj swój produkt tak, aby zadowolić użytkowników. Jeśli zdobędziesz użytkowników, wszystko inne przyjdzie samo. A jeśli nie, nikogo nie będzie obchodzić, jak pocieszająco ortodoksyjne były twoje wybory technologiczne.
Koszt bycia przeciętnym
Ile tracisz, używając mniej potężnego języka? Istnieją na to pewne dane.
Najwygodniejszym miernikiem mocy jest prawdopodobnie wielkość kodu. Celem języków wysokiego poziomu jest dostarczenie większych abstrakcji – większych cegieł, że tak powiem, dzięki czemu nie potrzebujesz ich tak wielu, aby zbudować ścianę o określonym rozmiarze. Im potężniejszy język, tym krótszy program (nie tylko w znakach, oczywiście, ale w odrębnych elementach).
Jak potężniejszy język umożliwia pisanie krótszych programów? Jedną z technik, którą możesz zastosować, jeśli język na to pozwala, jest coś, co nazywa się programowaniem od dołu do góry. Zamiast po prostu pisać swoją aplikację w bazowym języku, budujesz na nim język do pisania podobnych programów, a następnie piszesz w nim swój program. Połączony kod może być znacznie krótszy niż gdybyś napisał cały program w bazowym języku – w rzeczywistości tak działają algorytmy kompresji. Program od dołu do góry powinien być również łatwiejszy do modyfikacji, ponieważ w wielu przypadkach warstwa językowa wcale nie będzie musiała się zmieniać.
Wielkość kodu jest ważna, ponieważ czas potrzebny na napisanie programu zależy głównie od jego długości. Jeśli twój program byłby trzy razy dłuższy w innym języku, zajmie trzy razy więcej czasu na napisanie – i nie możesz tego obejść, zatrudniając więcej ludzi, ponieważ powyżej pewnego rozmiaru nowi pracownicy są faktycznie stratą netto. Fred Brooks opisał to zjawisko w swojej słynnej książce The Mythical Man-Month, a wszystko, co widziałem, potwierdza to, co powiedział.
Jak więc krótsze są twoje programy, jeśli piszesz je w Lispie? Większość liczb, które słyszałem dla Lispa w porównaniu do C, na przykład, wynosi około 7-10 razy. Ale niedawny artykuł o ITA w magazynie New Architect mówił, że „jedna linia Lispa może zastąpić 20 linii C”, a ponieważ ten artykuł był pełen cytatów z prezesa ITA, zakładam, że uzyskali tę liczbę od ITA. Jeśli tak, to możemy pokładać w niej pewne zaufanie; oprogramowanie ITA zawiera dużo kodu C i C++, a także Lisp, więc mówią z doświadczenia.
Moim zdaniem te mnożniki nawet nie są stałe. Myślę, że rosną, gdy napotykasz trudniejsze problemy, a także gdy masz mądrzejszych programistów. Naprawdę dobry haker potrafi wycisnąć więcej z lepszych narzędzi.
Jako jeden punkt danych na krzywej, w każdym razie, jeśli miałbyś konkurować z ITA i zdecydowałbyś się napisać swoje oprogramowanie w C, oni mogliby rozwijać oprogramowanie dwadzieścia razy szybciej niż ty. Jeśli poświęciłbyś rok na nową funkcję, oni mogliby ją powielić w mniej niż trzy tygodnie. Podczas gdy gdyby oni poświęcili tylko trzy miesiące na opracowanie czegoś nowego, tobie zajęłoby to pięć lat, zanim też byś to miał.
I wiesz co? To najlepszy scenariusz. Kiedy mówisz o stosunkach wielkości kodu, domyślnie zakładasz, że faktycznie możesz napisać program w słabszym języku. Ale w rzeczywistości istnieją ograniczenia tego, co mogą zrobić programiści. Jeśli próbujesz rozwiązać trudny problem za pomocą języka, który jest zbyt niskopoziomowy, dochodzisz do punktu, w którym jest po prostu za dużo do ogarnięcia w głowie naraz.
Tak więc, kiedy mówię, że stworzenie czegoś, co ITA mogłoby napisać w Lispie w trzy miesiące, zajęłoby ich wyimaginowanemu konkurentowi pięć lat, mam na myśli pięć lat, jeśli nic się nie stanie. W rzeczywistości, sposób, w jaki rzeczy działają w większości firm, każdy projekt rozwojowy, który zająłby pięć lat, prawdopodobnie nigdy nie zostanie ukończony.
Przyznaję, że to ekstremalny przypadek. Hakerzy ITA wydają się być niezwykle inteligentni, a C jest dość niskopoziomowym językiem. Ale na konkurencyjnym rynku nawet różnica od dwóch do trzech do jednego wystarczyłaby, aby zagwarantować, że zawsze będziesz w tyle.
Przepis
To jest rodzaj możliwości, o których szef z zakutą głową nawet nie chce myśleć. I dlatego większość z nich tego nie robi. Ponieważ, wiesz, kiedy przychodzi co do czego, szef z zakutą głową nie ma nic przeciwko temu, aby jego firma została pokonana, dopóki nikt nie udowodni, że to jego wina. Najbezpieczniejszym planem dla niego osobiście jest trzymanie się blisko środka stada.
W dużych organizacjach fraza używana do opisania tego podejścia to „najlepsza praktyka branżowa”. Jej celem jest zwolnienie szefa z zakutą głową z odpowiedzialności: jeśli wybierze coś, co jest „najlepszą praktyką branżową”, a firma przegra, nie można go winić. Nie wybrał on, wybrała branża.
Wierzę, że ten termin był pierwotnie używany do opisu metod księgowych i tak dalej. Co to mniej więcej oznacza: nie rób nic dziwnego. A w księgowości to prawdopodobnie dobry pomysł. Określenia „najnowocześniejszy” i „księgowość” nie brzmią dobrze razem. Ale kiedy importujesz to kryterium do decyzji dotyczących technologii, zaczynasz uzyskiwać niewłaściwe odpowiedzi.
Technologia często powinna być najnowocześniejsza. W językach programowania, jak zauważył Erann Gat, to, co faktycznie daje „najlepsza praktyka branżowa”, to nie najlepsze, ale jedynie średnie. Kiedy decyzja powoduje, że rozwijasz oprogramowanie w ułamku tempa bardziej agresywnych konkurentów, „najlepsza praktyka” jest błędną nazwą.
Oto dwie informacje, które uważam za bardzo cenne. Właściwie wiem to z własnego doświadczenia. Numer 1, języki różnią się mocą. Numer 2, większość menedżerów celowo to ignoruje. Pomiędzy nimi te dwa fakty to dosłownie przepis na zarabianie pieniędzy. ITA jest przykładem tego przepisu w działaniu. Jeśli chcesz wygrać w branży oprogramowania, po prostu podejmij najtrudniejszy problem, jaki możesz znaleźć, użyj najpotężniejszego języka, jaki możesz zdobyć, i czekaj, aż szefowie z zakutymi głowami twoich konkurentów powrócą do średniej.
Dodatek: Moc
Jako ilustrację tego, co mam na myśli mówiąc o względnej mocy języków programowania, rozważmy następujący problem. Chcemy napisać funkcję generującą akumulatory – funkcję, która przyjmuje liczbę n i zwraca funkcję, która przyjmuje inną liczbę i i zwraca n zwiększone o i.
(To jest zwiększone o, a nie plus. Akumulator musi akumulować.)
W Common Lisp byłoby to (defun foo (n) (lambda (i) (incf n i)))
, a w Perlu 5, sub foo { my ($n) = @_; sub {$n += shift} }
, który ma więcej elementów niż wersja Lisp, ponieważ w Perlu trzeba ręcznie wyodrębniać parametry.
W Smalltalk kod jest nieco dłuższy niż w Lispie foo: n |s| s := n. ^[:i| s := s+i. ]
, ponieważ chociaż ogólnie zmienne leksykalne działają, nie można przypisać do parametru, więc trzeba utworzyć nową zmienną s.
W Javascript przykład jest ponownie nieco dłuższy, ponieważ Javascript zachowuje rozróżnienie między instrukcjami a wyrażeniami, więc potrzebne są jawne instrukcje return
, aby zwracać wartości: function foo(n) { return function (i) { return n += i } }
(Żeby być sprawiedliwym, Perl również zachowuje to rozróżnienie, ale radzi sobie z tym w typowy dla Perla sposób, pozwalając pominąć return
s).
Jeśli spróbujesz przetłumaczyć kod Lisp/Perl/Smalltalk/Javascript na Pythona, napotkasz pewne ograniczenia. Ponieważ Python nie w pełni obsługuje zmiennych leksykalnych, musisz utworzyć strukturę danych do przechowywania wartości n. I chociaż Python ma typ danych funkcji, nie ma dla niego reprezentacji literałowej (chyba że ciało jest tylko jednym wyrażeniem), więc musisz utworzyć nazwaną funkcję do zwrócenia. Oto co otrzymasz: def foo(n): s = [n] def bar(i): s[0] += i return s[0] return bar Python użytkownicy mogą zasadnie zapytać, dlaczego nie mogą po prostu napisać def foo(n): return lambda i: return n += i albo nawet def foo(n): lambda i: n += i, a moje przypuszczenie jest takie, że prawdopodobnie zrobią to pewnego dnia. (Ale jeśli nie chcą czekać, aż Python ewoluuje do Lispa, mogliby po prostu...)
W językach obiektowych można w ograniczonym zakresie symulować zamknięcie (funkcję odwołującą się do zmiennych zdefiniowanych w otaczających zakresach) poprzez zdefiniowanie klasy z jedną metodą i polem zastępującym każdą zmienną z otaczającego zakresu. To sprawia, że programista wykonuje analizę kodu, która zostałaby wykonana przez kompilator w języku z pełną obsługą zakresu leksykalnego, i nie zadziała, jeśli więcej niż jedna funkcja odwołuje się do tej samej zmiennej, ale w prostych przypadkach, takich jak ten, jest to wystarczające.
Eksperci Pythona wydają się zgadzać, że jest to preferowany sposób rozwiązania problemu w Pythonie, pisząc albo def foo(n): class acc: def init(self, s): self.s = s def inc(self, i): self.s += i return self.s return acc(n).inc albo class foo: def init(self, n): self.n = n def call(self, i): self.n += i return self.n Włączam je, ponieważ nie chciałbym, aby zwolennicy Pythona powiedzieli, że źle przedstawiłem język, ale oba wydają mi się bardziej skomplikowane niż pierwsza wersja. Robisz to samo, tworząc oddzielne miejsce do przechowywania akumulatora; to tylko pole w obiekcie zamiast głowy listy. A użycie tych specjalnych, zarezerwowanych nazw pól, zwłaszcza __call__
, wydaje się być trochę hackiem.
W rywalizacji między Perlem a Pythonem, twierdzenie hakerów Pythona wydaje się być takie, że Python jest bardziej elegancką alternatywą dla Perla, ale ten przypadek pokazuje, że moc jest ostateczną elegancją: program Perla jest prostszy (ma mniej elementów), nawet jeśli składnia jest nieco brzydsza.
A co z innymi językami? W innych językach wspomnianych w tej prezentacji – Fortran, C, C++, Java i Visual Basic – nie jest jasne, czy można faktycznie rozwiązać ten problem. Ken Anderson mówi, że poniższy kod jest jak najbliżej, jak można się zbliżyć w Javie:
public interface Inttoint {
public int call(int i);
}
public static Inttoint foo(final int n) {
return new Inttoint() {
int s = n;
public int call(int i) {
s = s + i;
return s;
}};
}
To nie spełnia specyfikacji, ponieważ działa tylko dla liczb całkowitych. Po wielu wymianach e-maili z hakerami Javy, powiedziałbym, że napisanie odpowiednio polimorficznej wersji, która zachowuje się jak poprzednie przykłady, jest czymś pomiędzy cholernie niezręcznym a niemożliwym. Jeśli ktoś chce napisać taką wersję, byłbym bardzo ciekawy, żeby ją zobaczyć, ale osobiście się poddałem.
Nie jest dosłownie prawdą, że nie można rozwiązać tego problemu w innych językach, oczywiście. Fakt, że wszystkie te języki są równoważne Turingowi, oznacza, że ściśle rzecz biorąc, można napisać dowolny program w każdym z nich. Więc jak byś to zrobił? W skrajnym przypadku, pisząc interpreter Lispa w mniej potężnym języku.
To brzmi jak żart, ale zdarza się to tak często w różnym stopniu w dużych projektach programistycznych, że zjawisko to ma swoją nazwę, Dziesiąta Zasada Greenspuna:
Każdy wystarczająco skomplikowany program w C lub Fortranie zawiera ad hoc, nieformalnie zdefiniowaną, pełną błędów, wolną implementację połowy Common Lispa.
Jeśli próbujesz rozwiązać trudny problem, pytanie nie brzmi, czy użyjesz wystarczająco potężnego języka, ale czy (a) użyjesz potężnego języka, (b) napiszesz de facto interpreter dla jednego, czy (c) sam staniesz się ludzkim kompilatorem dla jednego. Już widzimy, że zaczyna się to dziać w przykładzie z Pythonem, gdzie w efekcie symulujemy kod, który kompilator wygenerowałby do implementacji zmiennej leksykalnej.
Praktyka ta jest nie tylko powszechna, ale i zinstytucjonalizowana. Na przykład w świecie obiektowym dużo mówi się o „wzorcach”. Zastanawiam się, czy te wzorce nie są czasem dowodem działania przypadku (c), ludzkiego kompilatora. Kiedy widzę wzorce w moich programach, uważam to za znak kłopotów. Kształt programu powinien odzwierciedlać tylko problem, który musi rozwiązać. Wszelka inna regularność w kodzie jest dla mnie znakiem, że używam zbyt mało potężnych abstrakcji – często, że ręcznie generuję rozszerzenia jakiegoś makra, które muszę napisać.
Przypisy
-
Procesor IBM 704 miał rozmiar lodówki, ale był znacznie cięższy. Procesor ważył 3150 funtów, a 4K pamięci RAM znajdowało się w osobnym pudle ważącym kolejne 4000 funtów. Sub-Zero 690, jedna z największych domowych lodówek, waży 656 funtów.
-
Steve Russell napisał również pierwszą (cyfrową) grę komputerową, Spacewar, w 1962 roku.
-
Jeśli chcesz oszukać szefa z zakutą głową, aby pozwolił ci pisać oprogramowanie w Lispie, możesz spróbować powiedzieć mu, że to XML.
-
Oto generator akumulatorów w innych dialektach Lispa: Scheme: (define (foo n) (lambda (i) (set! n (+ n i)) n)) Goo: (df foo (n) (op incf n _))) Arc: (def foo (n) [++ n _])
-
Smutna opowieść Eranna Gata o „najlepszej praktyce branżowej” w JPL zainspirowała mnie do zajęcia się tym powszechnie błędnie stosowanym zwrotem.
-
Peter Norvig stwierdził, że 16 z 23 wzorców w Design Patterns było „niewidocznych lub prostszych” w Lispie.
-
Dzięki wielu osobom, które odpowiedziały na moje pytania dotyczące różnych języków i/lub przeczytały wersje robocze tego tekstu, w tym Kenowi Andersonowi, Trevorowi Blackwellowi, Erannowi Gatowi, Danowi Giffinowi, Sarah Harlin, Jeremy'emu Hyltonowi, Robertowi Morrisowi, Peterowi Norvigowi, Guyowi Steele'owi i Antonowi van Straatenowi. Nie ponoszą oni winy za żadne wyrażone opinie.
Powiązane:
Wielu ludzi odpowiedziało na tę prezentację, więc przygotowałem dodatkową stronę, aby zająć się poruszonymi przez nich kwestiami: Re: Revenge of the Nerds.
Wywołało to również obszerną i często użyteczną dyskusję na liście mailingowej LL1. Zobacz w szczególności pocztę Antona van Straatena na temat kompresji semantycznej.
Niektóre z wiadomości na LL1 skłoniły mnie do głębszego zbadania tematu mocy języków w Succinctness is Power.
Większy zbiór kanonicznych implementacji benchmarku generatora akumulatorów zebrano na osobnej stronie.
Tłumaczenie na japoński, Tłumaczenie na hiszpański, Tłumaczenie na chiński