How Yandex taught me how to interview programmers

After I set out my story of “employment” in Yandex in the commentary on the sensational note “How I worked for 3 months in Y. Market and quit”, it would be unfair to conceal the benefit that I took from my Yandex.Message experience.



My job responsibilities include technical interviewing of candidates for the Fullstack JavaScript / TypeScript Developer position, I have been actively involved in this business (is it worth saying that I’m a little fed up?) For more than a year, I have over 30 technical interviews.



Earlier in a technical interview, I asked the candidate rather stupid questions like “what is a closure”, “how is inheritance implemented in JavaScript”, “here is such a table in the database with such indexes, please tell us how to speed up such request ”, which, although they helped to identify the candidate’s engineering abilities, didn’t allow them to conclude how well a person can solve problems and how quickly he can figure out an existing code. What could not but lead to sad consequences ...



But everything changed after I went through four rounds of technical interviews at Yandex.



Usually stones are sent to the garden of Yandex interviewers for:



1. Tasks that do not have practical value;

2. The need to solve these problems on pieces of paper with a pencil or on a blackboard.



It is already 2019 and it is time to launch a separate production line for casting boilers in hell for those who force people to write text by hand, not to mention the code. Each person writes in different ways, and in preparing the text for this note, I had to rewrite, for example, specifically this paragraph six times — if I wrote notes for Habr on paper, I would not write notes for Habr.



But I do not agree with the thesis about the practical futility of Yandex tasks. Even the routine development is no, no, but it will set you a task that has several solutions. You don’t need to think about one for a long time, but it is not optimal in terms of code size, performance or expressiveness. The other is just the opposite, but it requires the programmer some experience in building efficient and understandable algorithms. Here is an example from an interview:



/*    getRanges,    : */ getRanges([0, 1, 2, 3, 4, 7, 8, 10]) // "0-4,7-8,10" getRanges([4,7,10]) // "4,7,10" getRanges([2, 3, 8, 9]) // "2-3,8-9"
      
      





On this task, I seriously blunted, and decided it is not the most beautiful way. The solution, unfortunately, has not been preserved, so I will give a solution to one of our candidates:



 function getRanges(arr: number[]) { return arr.map((v, k) => { if (v - 1 === arr[k - 1]) { if (arr[k + 1] === v + 1) { return '' } else { return `-${v},` } } else { return v + ',' } }).join('').split(',-').join('-') }
      
      





Of the minuses: accessing the array at a nonexistent index and ugly string manipulation: join-split-join. This solution is also incorrect, because with the example getRanges ([1, 2, 3, 5, 6, 8]), “1-3,5-6,8,” is returned, and to “kill” the comma at the end, you need further improve the conditions by complicating the logic and reducing readability.



Here is a Yandex-style solution:

 const getRanges = arr => arr .reduceRight((r, e) => r.length ? (r[0][0] === e + 1 ? r[0].unshift(e) : r.unshift([e])) && r : [[e]], []) .map(a => a.join('-')).join(',')
      
      





Will google help write such elegant solutions? To produce such a code, you need two components: experience with many algorithms and excellent knowledge of the language. And this is exactly what Yandex recruiters warn about: they will ask you about algorithms and language. Yandex prefers to hire developers who can write cool code. Such programmers are effective, but, most importantly, interchangeable: they will write about the same solutions. Less theoretically savvy developers on one task are able to give out dozens of diverse, sometimes simply amazing solutions: one of the candidates for our vacancy wrapped up such a crutch that my eyes climbed.



UPD: as the user MaxVetrov noted , my solution is incorrect:



 getRanges([1,2,3,4,6,7]) // 1-2-3-4,6-7
      
      





Thus, I myself have not been able to properly solve this problem so far.



UPD2: In general, the comments convinced me that this code turned out to be bad, even if it worked correctly.



It was not in vain that I spent time translating paper in the Yandex office, because during this lesson I understood how to become an effective interviewer myself. I took the idea and format of their tasks as a basis, but:





One of the objectives of our technical interview:



 let n = 0 while (++n < 5) { setTimeout(() => console.log(n), 10 + n) } //     ?
      
      





I think this is a very significant test for the JavaScript developer. And the point here is not the closure and understanding of the differences between preincrement and postincrement, but that, for some inexplicable reason, a quarter of the interviewees believe that console.log will execute before the cycle ends. I am not exaggerating. These people have at least two years of resume and work experience, and they successfully solved other tasks that were not tied to callbacks. Either this is a new generation of JavaScript developers who grew up on async / await, who heard something else about Promise, but callbacks for them - like a disk phone for a modern teenager - will dial the number, even if not the first time, but will not understand how it works and why.



This task has a continuation: you need to add the code so that console.log also runs inside setTimeout, but the values ​​1, 2, 3, 4 are displayed in the console. The saying “live, learn,” is appropriate here, because once one of the interviewees proposed such a solution:



 setTimeout(n => console.log(n), 10 + n, n)
      
      





And then I found out that setTimeout and setInterval pass the third and subsequent arguments to the callback. It's a shame, yes. By the way, knowledge turned out to be useful: I have used this feature more than once.



But I borrowed this task from Yandex as it is:



 /*    fetchUrl,     .  fetchUrl     fetch,    Promise      reject */ fetchUrl('https://google/com') .then(...) .catch(...) // atch     5       fetchUrl
      
      





Here are tested skills with Promise. Usually I ask you to solve this problem on pure promises, and then using async / await. With async / await, the solution is intuitively simple:



 function async fetchUrl(url) { for (let n = 0; n < 5; n++) { try { return await fetch(url) } catch (err) { } } throw new Error('Fetch failed after 5 attempts') }
      
      





You can also apply the saying “live, learn,” to this decision, but with respect to my Yandex interviewer: he didn’t specify that async / await can / cannot be used, and when I wrote this solution, he was surprised: “I didn’t work with async / await, I didn’t think it could be solved so easily. " He probably expected to see something like this:



 function fetchUrl(url, attempt = 5) { return Promise.resolve() .then(() => fetch(url)) .catch(() => attempt-- ? fetchUrl(url, attempt) : Promise.reject('Fetch failed after 5 attempts')) }'error'
      
      





This example is able to show how well a person understands promises, this is especially important on the back. Once I saw JavaScript code of a developer who didn’t fully understand the promises, prepared a promise for the sequelize transaction as follows:



 const transaction = Promise.resolve() for (const user of users) { transaction.then(() => { return some_action... }) }
      
      





And he wondered why only one user appeared in his transaction. One could use Promise.all, but one could know that Promise.prototype.then does not add another callback, but creates a new promise and it will be like this:



 let transaction = Promise.resolve() for (const user of users) { transaction = transaction.then(() => { await perform_some_operation... return some_action... }) }
      
      





This case made me think about complicating the task of understanding promises, but the candidate who refused to solve the problems helped me to formulate a new task, called them literally crap and said that he was used to working with real code, for which I rummaged for a couple of minutes in the source code of one of our projects, gave him a real code:



 public async addTicket(data: IAddTicketData): Promise<number> { const user = data.fromEmail ? await this.getUserByEmail(data.fromEmail) : undefined let category = data.category if (category === 'INCIDENT' && await this.isCategorizableType(data.type)) { category = 'INC_RFC' } const xml = await this.twig.render('Assyst/Views/add.twig', { from: data.fromEmail, text: data.text, category, user, }) const response = await this.query('events', 'post', xml) return new Promise((resolve, reject) => { xml2js.parseString(response, (err, result) => { if (err) { return reject(new Error(err.message)) } if (result.exception) { return reject(new Error(result.exception.message)) } resolve(result.event.id - 5000000) }) }) }
      
      





And asked to get rid of the async / await keywords. Since then, this task was the first and, in half of the cases, the last one at the interview - he is often really overwhelmed.



I myself have never solved this problem before writing this note and do it the first time for the third time (the first is too long, and in the second I did not notice one remaining await):







What conclusion can be drawn from all this? Interviews are interesting and useful ... Of course, if you are not urgently looking for work.



In the end, I’ll give you another task from the story with Yandex, I haven’t shown it to anyone * yet, I have taken care of what is called a special case. There is a set of banners, each banner has a “weight”, which indicates how often the banner will be displayed relative to other banners:



 const banners = [ { name: 'banner 1', weight: 1 }, { name: 'banner 2', weight: 1 }, { name: 'banner 3', weight: 1 }, { name: 'banner 4', weight: 1 }, { name: 'banner 5', weight: 3 }, { name: 'banner 6', weight: 2 }, { name: 'banner 7', weight: 2 }, { name: 'banner 8', weight: 2 }, { name: 'banner 9', weight: 4 }, { name: 'banner 10', weight: 1 }, ]
      
      





For example, if there are three banners with weights 1, 1, 2, their combined weight is 4, and the weight of the third is 2/4 of the total weight, so it should be displayed in 50% of cases. It is necessary to implement the getBanner function, which randomly, but taking into account weights, returns one banner for display. The solution can be checked in this snippet , where the expected and actual distribution is displayed.



UPD: they started not only to minus the article itself, but also to burn karma with leaps and bounds and I hid it in hiding material, which is ugly in relation to commentators. I correct this mudachism on my part.



All Articles