📜 ⬆️ ⬇️

“Extremely few people actually write backend on Kotlin” - an interview with Pasha Finkelstein

How to become a programmer from hopelessness and rise to the heights of success? Today in our virtual studio Pasha asm0dey Finkelstein answers questions. Pasha is one of the few who understands the creation of backends on Kotlin. In addition, he saws open source, actively participates in the life of the community, and, for a minute, he attended almost all of our Moscow Java conferences.


How to find time for commits


- There are several topics that can be discussed. First, you give a talk on Joker. Secondly, you are an active member of the community, constantly doing something. Thirdly, you constantly go to our conferences and for some reason think that this is good.


- About the fact that I am a member of the community. I, like most people, suffer from the impostor syndrome, I do not have the feeling that I’m doing a lot of useful things, especially for the Java community, especially recently. The last thing I did was useful for the community - the guys and I wrote a cool library for Spring, which is called spring-flow-state-machine , which allows us to manage the states of objects in the application. It is small and comfortable.


- Is this the same library that is being promoted on the main Spring?


- No, probably there is the Spring Statemachine, and it is wildly poor: it is very poorly written, it works very strangely and does something else. Spring Statemachine controls the state of the application when your application is some kind of finite statemachine and in different states it works differently. We did another thing: when you have some kind of entity, you set a life cycle for it and you can drive it through this life cycle. And our task is to think about how your transactions work and so on. We just have the @Transactional annotation in one right place, as you understand, and this is enough for us to say that it controls the state. The main thing is that there is a convenient DSL, which allows you to say where you can go from and where to go along the road.


“And I understand correctly that since this thing controls abstract things in you, you can do the same thing on it with what Spring Statemachine does?”


- Yes, really, there is a nuance. The main entity controlled by this thing seems to be an entity with id or something like that. This is really a very small library of two interfaces, two exceptiones and four classes.


- And the thing that Spring did - why do you think that this is something strange and it is not clear why it is necessary?


- It is clear why the necessary, just done very badly. It is stated as if it can do everything. Firstly, the examples that they have given in the documentation are not collected, and secondly, if you try to dig into the source code (and you always dig into Spring's source code when you want to do something, because not everything is written in the documentation), you find out that it is written the way you do not need to write better. By the way, it seems that to the Spring Batch is usually about the same claims.


“Did you use Spring Batch too?”


“I think I used almost everything in the Spring ecosystem.”


- And how - continue to use?


- You know, in the next job, I will not have Spring, I will have HER. It was not my choice. Probably, I would use Spring, but in fact I once told us in our podcast that now my favorite is the Jooby mic framework , which can do anything. It was written in one person by some Java champion. There is also dependency injection, which is built on Guice. It is cool, it has an ecosystem, in difference, by the way, from all other microframes.


- I see that there are two options - for Java and for Kotlin.


- I suspect that it is possible for anything. On Scala, probably, it is also possible, but you have to give up Guice, which probably does not work with Scala. I, as usual, love constructor injection with annotations. And this is done in Guice.


- And if such an application can be collected using GraalVM statically - it will be just space.


“I don’t think you'll win much there.” It's like Spring - of course, you win something by performance.


- To raise the speed of a startup, for example.


- Of course, in production, we are dragging some kind of marginal piece written by one Java champion, and we will be there fighting for the speed of a startup!


- Clear.


- Probably, the guys who really need the speed of a startup, take some Azul Zing and tyunyt them something on a naked SE written.


- Did I understand correctly that the library that you wrote is open source and so on?


- Yes, she is in jeopardy with my past employer and somehow belongs to him because it was used in the working draft.


- Clear. That is, you wrote it during business hours.


- Yes, they wrote during working hours.


- What do you think, is there any prospect of writing software during off-hours?


- I write software during off-hours, however, now this is not real software. And the last thing I wrote ... now in Russia there is a problem with locks, but for me the actual problem is to bypass these locks. There is a wonderful proxy server called 3proxy , so I wrote Feds, Ubuntu, Debian, and Centos Ansible playbook , which was tested at all ends, and shared it on GitHub. And in Ansible Galaxy also shared.


“Where do you find time for all this?”


- I, like everyone, have problems with time, because I get tired at work, I have a family, I need to spend at least some time with my family. But on weekends the family sleeps late, and I am a morning bird and I wake up early, my head works well in the morning, and I can do a lot of things. Sometimes I write something even before work.


- What would you advise to those who want to start writing something in the open source, but it does not work?


- Ask yourself why it does not work? And depending on the answer to the question, try to write something.


- There is a standard answer: there is no time, it is not clear where I need this.


- About “there is no time”, it happens that there is really no time, and sometimes it happens that there is not really no time. “No time” is such a way to say that I have no motivation. This is absolutely well understood, and not a fact that we must fight this. Not all people are required to commit to open source. This is a cool way to do something for yourself and for the community, but nobody owes anything to anyone.


If it is not clear who needs you, the question is, again, why. There are two reasons: you still have very little time at all doing something like this in programming, and it seems to you that this is something extremely boring, and then probably true, at this stage you cannot help anyone. Although I know people, for example, Slava Semushin - the first thing he did - began contributing to Alt Linux before he started programming somewhere in production. It was his way to programming (this is a joke, of course). But he really did not understand anything when he started, understood along the way. And this is no joke.


It happens that people are engaged in an enterprise for a long time and do not understand what they can do. In this situation, I was very long. Another 4 years ago for sure. I have been in the Enterprise for 6 years and did not understand what I can do. But over time, I saw that this component of the system that I want to rewrite, in principle, can be divided into a separate module and its replacement. This must be agreed with the employer. The main thing is to take the first step.


Sometimes the employer does not want to coordinate, it is quite a lively topic. For example, Tinkoff is not eager to open his own backend components, although it would theoretically be possible.


You begin to see opportunities with time. The main thing is to allocate it in a separate component. If the employer does not allow it, you will use it inside the employer, there will be an internal open source, as they do in some Zalando. Then on the next project you will see that this thing can be highlighted too.


- Wow. Or write it again, for example.


- About the "write again." One of the problems that we solved at the last place was logging. It is clear that all the big ones are confused with centralized logging, without this there is no life at all. The standard solution for this is ELK stack, ElasticSearch, Logstash, Kibana. Only there is a small problem - it is Logstash. ElasticSearch, Kibana work well, and Logstash - not very. It either does not work persistently or is very slow. If he is not working persistently, then the size of his queue is 5,000 messages. Stuffed more - the messages begin to drop. To put it mildly, an unpleasant feature. So we had Kafka instead. In Sberbank, we wrote our Kafka logback appender , which is also zaopensorshen and works quite well for yourself.


- So that's who it was!


- There are already three of them, I'm not sure that you saw ours exactly.


- Okay. Returning to the topic of open source and logging. And why do people use centralized loggers in the enterprise all the time, try to write them themselves, if there are Linux and the methods invented by the grandfathers - to fill everything into a file, for example?


- I do not know normal centralized logging in Linux. There is an rsyslog, which is a good format, but, to put it mildly, is not adapted, for example, to Java, because if you look at some setraces, it will suddenly become clear that the spectra is a lot of lines, and not one. So, in rsyslog, these will be separate log entries. If this is all mixed up in one big pile, it seems to me that it will be painful for everyone.


- Yes, probably.


- And I, frankly, do not know what guarantees rsyslog provides. I usually get tired when I have some solution, I try to find its place in the CAP theorem. And I rsyslog just did not find this information, maybe I was looking bad. But quickly I could not identify it. Does he guarantee delivery to us and for how long? What time stamps will there be at the fact that he hisses somewhere? If he guarantees, this is not bad, if you normally keep logs with all sorts of spans and traces, then you will still figure out what he did there. If he does not guarantee delivery, it is quite a disaster. Kafka in this sense, I trust more, but she has more bandwidth. This is despite the fact that I do not know Kafka, to be honest.


- There you need to somehow divide, probably, the logs into critical / non-critical, because if the logging server still crashes, you will never know what happened.


As long as you and I are talking about Java, everything is quite simple. We run in some Docker and we have logs in stdout entered from the ERROR level. And everything flows into Kafka, starting from debag or from treys. I’m the person whose debug logs never actually turn off. I have never lived up to this bright moment, when I realized - everything, I no longer use debase logs to analyze problems.


- Are you not afraid that the bandwidth, for example, of the Kafka network will simply take and end if all these terabytes of logs fly there?


- We have always been too small, we had 100 GB of logs per day, and 100 GB - what kind of volume? The main problem is how to store it, not how to transfer it. Did you really have terabytes of logs?


“I don’t know how much it was in the storage expression on disk, but I know that sometimes the system started so slowly that we didn’t wait for it to start. Someone accidentally put either debag, or trace, and that's it.


- Wait. It seems to me, or did you go the wrong way when you made the logs synchronous over the network?


- No, they just poured it in Kafka.


- But synchronously? It is unlikely that you had the option to do asynchronously, you would have to wait for Kafka's ACK from at least one node, even if you are completely stubborn and you don’t want a guarantee on the logs. At least one ACK you still waited.


- Maybe yes.


- I say that, because we wrote logback appender, I understand how we were guided. But we wrapped this appender in AsyncAppender, and then experimentally calculated how many messages we have in peak, and the buffer in this AsyncAppender was unscrewed by a bit more, such as there is a peak plus 10%. It turned out that our application is not blocked.


- What terrible stories you have. And why is it all not standard in Java, why do you need to write with your hands?


- Because you need to write with your hands in all languages, only here in Java it is at least clear where to write. And if you take any advantages, I do not know where to start.


- So you think there are not any libraries that can be added to Maven, and everything will be established by itself?


- Let's start with the fact that they do not have a normal Maven, because they have Ninja, Make, CMake, qmake and everything is a little hard with Maven. Of course, they have libraries for logging, they also exist in Java, and it is clear how to attach appenders to them. And in Java they are beautiful, flexible, they usually work with facades and all that. And if you look at some Rust, there is something very sad, sadness. I don't know where to put and record my loggers. I hardly mastered how to configure them.


- So you really like Java?


- I like the ecosystem. I like Kotlin as a language and Java as an ecosystem. By the way, not a single language, better than Kotlin, I do not know at all now. Once I was hanging on Groovy, I even have a T-shirt from Baruch with the Groovy sign.



Why precisely Kotlin?


- Choosing between Java, Scala, Groovy and Kotlin - why exactly Kotlin?


- The perfect combination of good syntax and difficulty to shoot yourself in the foot. I do not like a rock, because it is very easy to shoot a leg, and sometimes not to myself, but to my neighbor. You add implicit variables, everything works magically for you, and he has hell in debug. You can, of course, agree not to use them, then you lose some of the charms of the Rock. Of course, if you are very smart, you use macros in Scala and no one except you understands how it works.


- In the Rock completed the macros, it turns out?


- I want to say that they work, but I do not want to say that they can be used. And if you're even smarter, then you use a library like Cats. But this is probably at the same level, when you became a little smarter and you are already bored with using scalaz, and you use Cats. But nobody can read this code except you.


- And other adepts Cats.


“You say it like it's true, but it's not true, because other Cats followers use it differently.”


- This is part of the power of this system.


- Very powerful, reminds BFG .


“With which you shoot at your feet.”


“Sometimes for yourself, sometimes for others, as lucky.” By the way, I noticed that people who write on the Rock sometimes forget to think. They have such beautiful concepts that they can make 2 inserts in a row in different places and not think about the fact that all this needs to be wrapped in a transaction. But they have beautiful calls to the database with some kind of slick (or whatever it is now fashionable to work with SQL in Scala).


- But you can do it in two places in Java ...


“But in Java I have Spring, and I annotate @Transactional.” It's not customary to do complicated things in Java. In Kotlin, it seems, by the way, too. And in the Rock is accepted. As for Gruvy, this is a cool language, of course, until you get into baytkod on one side or you don’t bother with the fact that you don’t understand what type of object you have. They also sometimes like to shout that any valid java code is any valid groove code, but this is not true, because anonymous inner classes are not working there. Cannot be taken and instantiated. You have to create a lambda and then cast it. And so I choose Kotlin, he has a slightly different syntax than in Java, but there is a converter there, if you really want to. On the other hand, no new complexity. There is nothing that you could not ctrl-click on something and not go. You ctrl-click on double equals and switch to the equals method.


- Such a slippery moment: do you trust JetBrains? Now many do not really trust Java, because it is Oracle, and what to do with JetBrains? There are normal guys sitting, develop language?


- I do not want to spoil a piece of the report, but in general I like the way they speak, and I like what I see. I do not have the option "not to trust." Okay, well, I don’t trust JetBrains, then everything becomes completely sad, because what other languages ​​do we have developed by some relatively sane people? Probably Rust (developed by Mozilla), Golang is developed by Google. Plus, they are all developed by communities including, even Java. It is just not clear who and why can be trusted. I have problems with paranoia, I do not know how.


On the first day of the Joker conference (October 19-20, 2018), Pasha will talk about backlinks to Kotlin in the report “Kotlin - 2 years in production and not a single gap” . Joker is one of our main conferences, and we constantly write about it in Habré. If possible, be sure to come.

- Recently, Baruch and I discovered a certain study on Kotlin, which shows that the program on Kotlin in 40 of their parrots (study parrots) is better than the program on Java.


- Not better, but in short, if my memory serves me.


- There and in short, and better. They measured it all in code smell. I do not know how to say it. Perhaps, in a sense, this can be replaced by the word "better."


- Well. Only you know, in any comparison of two languages ​​there is one small problem. Did you realize the same task and did you have the same measurement tools? Is it true that the smild code measures equally well in Java and Kotlin? Or have they been writing to Java for the last 20 years, and the smolly code learned to find 800, and they have been writing to Kotlin for 2 years and learned how to find 20?


- Wow, this is a very good question. But it’s not the numbers that they derived that are interesting, but what questions they drew up. There are only 5 of them, I want to ask them to you too. What level of Kotlin adaptation anywhere in the area? It depends on how many people you work with, how many projects, how many examples.


- It is clear that the entire Android almost moved to Kotlin (modern development), and the backend is moving very slowly. One of the goals of my report is to speed up this process. When I came to HH for an interview and said that for the last two years I have been writing a backend on Kotlin, they told me, but what can I do? This is only for mobile phones! Employers really do not know that this is possible. Probably the employees also do not know. It’s hard to talk about the volume of adaptation, I think that in Android it is 80-90%, and in the backend it is 10 percent maximum, I guess. Yes, what 10! If you count all the legacy systems, two percent will not be there either. But if you count only new projects, then maybe 10 percent.


- That is, at least such a thing as Kotlin on the back end already exists?


- Yes, I wrote in the Real Estate Center from Sberbank. And before that, too, wrote.


- Good. It's just that many people say that this is JetBrains marketing, and living people will react differently.


- Part of my report is devoted to the fact that in the end all of the company's java began to switch to Kotlin after our project successfully flew.


- Here, and the next question arises: what percentage of the code on Kotlin is the application? First, you can mix Java and Kotlin as a base. Secondly, Kotlin can be used as DSL and begin to rewrite everything into it.


- From my point of view, there are very few situations where you need to consciously interfere with Java and Kotlin. It makes sense to write a project on Kotlin entirely. Maybe if you feel that you are going to transfer the application to Kotlin entirely, you can first start writing new classes in Kotlin and then at some point start converting the old classes from Java to Kotlin and trim them with handles. But I must say that “Java2Kotlin” is a tool that is not so perfect.


- By "Java2Kotlin" you mean Ctrl C - Ctrl V in the Idea between files?


- Yes, but there still is some separate shortcut, I guess.


- By the way, I once tried to use Ctrl C - Ctrl V in the Idea for Scala, but it produced a rare treshnyak as a result.


- I also don’t like what Java2Kotlin does. I write better. Sometimes you can start with this, of course. You have two ways: either you convert the entire project, especially if it is not very big, and then you fix all the wrong places. Either you do it one by one: rewrite with your hands for a very long time, a lot of mechanical work. But you probably want to start somewhere.


- Whether there are any difficulties connected not with stupid syntax, but with the semantic part?


- Arises, I will tell about it in the report.


- Good. When Kotlin is added to the project, does the number increase or decrease? How much should Kotlin be in order for him to start eating everything?


- I have no such experience. We immediately wrote the application on Kotlin, except for one case. Are you familiar with the ecosystem of plugins for the Idea?


- Little.


- Some time ago, my friend, Slava Artemyev, wrote a relatively popular plugin to the Idea , which helps you navigate in the spring and EE-shnyh endpoints. There is some kind of tricky keyboard shortcut, you enter it, and you have a string for hints, you enter a path there, and you yourself have a controller and a method that is responsible for this path.


- In the Community Edition, this is probably a very useful thing.


- It works in Community Edition, and in Ultimate.


- Ultimate is not really needed, this feature is already in the Spring-plugin.


- There is no normally done, because it works only when you have an application running in Spring-plugin, if my memory serves me.


“The Spring plugin seems to have endpoints, but it works so well, I don't know.”


- So she doesn’t map to specific URLs. She knows the endpoints, but does she know how to search for them conveniently?


- No, you have to leaf through them. Okay, I get it, this plugin is better.


- Immediately the thing, which is like everything in the Idea, when you press Shift-Shift-Shift, starts writing something, Shift-Shift-Shift tells you everything that is in the Idea, and this thing itself is looking for an endpoint for you s and methods for urla. As usual, more or less case insensitive and all that.


- A, that is, it is Search Anywhere, only for urls and endpoints.


- So here. At first he wrote it in Java, because he started writing it before Kotlin became mainstream in the company. And then gradually we transferred it to Kotlin. And, probably, Kotlin ate everything when it became more than a third. When you see a real profit, you start moving there very quickly, because, on the one hand, you really reduce the amount of code, and on the other hand, the percentage of coverage of your code with tests increases, simply due to the lack of boilerplate, which you usually don’t you're testing.


- Does Kotlin's introduction lead to this or is it just that you turned your head on?


- It seems to me that the first. I do not think that in order to write on Kotlin or on Java, one must have a lot of brain.


- I'm not talking about that. There, including in this study, it is shown that the quality of the code has improved. And Baruch and I argued a little on the sidelines about this. It turns out that when a person starts writing in a new language and he specifically made such a sensible decision, then he and the code begins to write more meaningfully, and automatically it turns out that the code is better for him.


- Rather, my example refutes this, because, as you understand, no one starts writing the plug-in to the Idea, not including the head, because this is a thing that no one has ever done, except for people who specialize in this. And rare exceptions. Of course, there are community plugins. But you try to write immediately well. No, the fact is that it is easier to write well on Kotlin. There are not all these endless Java-puzzlers, the Idea also helps. If you write generic not broadly enough, it will tell you that there are invariants here, add the keyword in. Etc. On Kotlin, it's just easier to write good code. Rather, he turns out. Again, there are rare exceptions. For example, if you write in Haskell, how do you work with a collection? You say: map, fmap, filter and all that.


- The same on the Java streams.


- The problem is this: if you try to work ineffectively with a collection on Kotlin, it will be presented to you every time as a new collection, it will be added to a new sheet. Of course, there is a great thing in Kotlin - sequence. They are the same here in the Rock.


And then you start working with collections much more efficiently than java streams, and there is a benchmark about it, an unaccepted pull-request to Oleg Shelayev. But on GraalVM it is optimized to standing, as if we are performing one operation, and not several in the pipeline. This is what I posted in @graalvm_ru.


- Can you elaborate?


- Imagine a pipeline of operations on an array. Add 2 to each number in the array, then multiply the resulting number by 2 and then add 2 again. And then count the sum of the elements of the array.


- You can do this buffered, for example. You have an operation pipeline, you process the contents of the collection at your own pace.


- More or less, yes. In Java, we don’t have so many options: we either write for-cycles with conversion, or we write an optimized for-cycle, where we think in advance (and this was my baseline) what operations we will perform on each element. In fact, we multiply by 2 and add 6 in this case. This can be done on the streams, and in Kotlin - by two methods: sequences and non sequences. Nesikvensami, of course, turns out very slowly because of the wild overhead projector on everything. We create a bunch of unnecessary collections and all that. And with sequences works quickly. Faster than stream in Java. But the main thing is that with sequences, if you run under the Grail, your result improves to baseline.


- I mean, before the cycles?


- Yes, to one maximum optimal cycle.


- I think this is the thing that GraalVM wanted to make part of a paid edition.


- I think so too. In JDK10 with JVMCI, the optimization is not even close as good as in GraalVM EE, but much better than on the usual C2.


“I think you went to improve Community Edition to non-Community Edition, and I think they will kill you in the evening.”


- Not. I, unfortunately, did not even look at the code. No time. Of course, I lie in this place, but I have no time.


- You have no motivation! Based on your own terminology :-)


- Therefore, I specifically said that I was lying.


')

"Enter IT"


- Good. Let's talk on the abstract topic: how to enter the IT. The topic is burning, including for our readers on Habré. The question is: have you always worked as a programmer?


- Not. By education I am a psychologist in business. I graduated from the Faculty of Psychology at the Higher School of Economics with a degree in Business Psychology. And what I’ll say now is probably not suitable for any other people: when I finished my studies, I went to work first in technical support, and then became a sysadmin, I did quite well. In 2008 there was a crisis, I was cut down, and I began to look for a job, they called me to try to go as a programmer for very little money. Less than what I got admin. I went, tried it, and by chance I did it. I became a programmer because of hopelessness.


- What did you program on?


- The first 4 months were programmed in C ++ Builder, and then we had inside there opened small Java courses, which I, in my opinion, attended alone, to be honest. Java stuck me more than C ++ Builder, especially considering that when I wrote in C ++ Builder, I did not understand anything at all. I trained the icons and tried to link the actions. I even have an application written there that worked with the base. I no longer remember how it was written, I just know that it still works.


- There were components - VCL, here it is.


- In general, yes. But with the base I worked with real queries, I even generated them somewhere out there, collected them according to some rules. It was something scary, I had one big class with a window and a second class with SQL that was going to be executed. Now I would probably be careful not to write like that.


- Especially in C ++.


- Yes, anything, especially in Java, is not accepted here. There is no standard in C ++, as is customary, therefore, in principle, you can do as you like. And in Java, all sorts of MVC. Although, when you collect the interface in NetBeans, you see a code that you would never have written yourself. Not because it is bad, just very difficult. There all these GroupLayout or BorderLayout. I can not remember them, they are incredibly complex, from my point of view, still do not understand how it works. It’s good that I haven’t written a desktop for a long time, they wouldn’t take me anywhere as a desktop writer.


- Is Java on the desktop still alive, except for IDE-shek?


- A controversial issue. I, probably, would choose any Electron now.


- I would also. Moreover, you can simultaneously write both on the electron and in Java.


- I can write everything on Kotlin, from which an electron is generated later. With my language, everything is again quite simple: Kotlin compiles well into JavaScript. Connect libraries and that's it.


“And with my language called GraalVM it’s still easier, you can compile Electron with Grail and run JavaScript through polyglot runtime.”


- You will have to suffer from javascript. If no joke, of course you can. On Nashorn, you could do a very similar thing. Do we have a Truffle compiler for javascript? You can now continue the same thing not Nashorny, and Truffle.


- Immediately the question is that if you have Nashorn, you have to collect and suffer the stack. And here you can just copy the finished Electron, and it will start.


- Are you sure it will start?


- I saw in chatika how people launched the old version of Electron.


- Why old?


— Node.js, Graal Node.js, . .


— ?


- Yes. - .


— . — Node.js, Electron Builder, SQLite ORM . , , — SQLite — 10 . -, , . .


— , , , .


— . . , Java FX, , , , 10- . , JRE, . , — , . , -. , - , .


— , .


— - , - , . -, .


— - desktop environment , , -. , - to do list, - . , , , , . , , , . — F1, .


— , , . - – CLI-. , Task Warrior, . , .


— « ». , , , - . ?


— , ?


— .


— , .


— -. , ?


— , : «, , ?» : «». , - . - , - , . : « ?» , . : «, . , ?» «». « , ».


— : , , . ? , - ?


— , , , : . (5 , ), , - , , , , . , , , - , , , , .


— , , — , , ? , , ?


— , - . , . , . . , . . .


— : - , , - — SQL , . .


— ? — . , . — . - , , . .


— ! . - ?


— , , .


— - !


— , - . . , . , , . ( ). , , . , - , , , : « ?», , , Joker, .


— , , ?


— . - , , - , - . , JPoint. « » . , 45 .




— ? ? , , , .


— , , , , .


— - ?


— , . , . , , .


— .


— «», , . , , . , , . . -, , , .


, , . . , Azul Zing, . , , . - . : . . , , - , . , , . .


— .


— , . , Java — «Java Concurrency in Practice», Java — «High-Performance Java Persistence», Vlad Mihalcea ( ). , .


— ?


— . , : , , , , , . . . . , , , .


— , , , .


— , .


— , , .


— , . It is very important. , , . - , . . , , . , , , , . - , , , - , , - , . , , . , , . , , . , , . , 6.


— , :-) ! Joker, .

Source: https://habr.com/ru/post/421749/


All Articles