Wednesday, December 14, 2022

Unit tests are not the magic solution to code integrity

I’ve seen dev teams constantly tout the importance of having as many unit tests in a codebase as possible. 

This became such an important metric for one such group, that leadership overseeing the team started a competition between the developers to track the “leaders” who had the most unit tests. 


Engineering VPs began frequently sending out a chart listing the top unit test producers for each sprint. Each chart would be broken down like this:


Name

# of unit tests committed during sprint

Person A

12

Person B

5

Person C

2

Person D

2


They would stress in their communications for everyone to keep up the good work, and keep adding more “good” unit tests. “We need more and more good unit tests” they would constantly reiterate and repeat over and over in nearly every email or meeting regarding improving code quality of the codebase. 


Here’s the problem with this type of practice: this doesn’t promote the production of more, “good”, quality unit tests — it promotes the production of more frivolous unit tests.


Hyper-focusing on just the volume of unit tests goes against one of the main reasons they exist: to increase the testability, maintainability, extensibility, and clarity of code. If you’re throwing unit tests out there just for the sake of increasing unit test count, you’re actually hindering your ability to achieve these things — not helping them. Ironically, you end up adding one of the biggest things you aim to get rid of: wasteful, glitchy, code overhead that has no purpose. Additionally, keep in mind that unit tests need to get executed in order to actually perform the validation testing that they’re written to do. The unit tests that a developer coded just to reach an inconsequential goal and gain a top spot on a leader board is going to drag down the runtime of build tests (among other things) as well as impede the runtime of their own local testing (which can result in the delay of them rolling out bug fixes and new features).


With any mature codebase, there’s often a great deal of bloat or oversights inherit in the code resulting from past development decisions that prioritized speed over quality and thoroughness. This “Tech Debt” makes it harder for developers to test, debug, and extend the surrounding code due to the bloat and verbose nature of it. Unfortunately, addressing tech debt isn’t as “flashy” of an endeavor as adding new features to the product. At the same time, however, stakeholders understand that this bloat makes it harder to maintain and support the system, and impairs the organization’s ability to ship new functionality and bug fixes to customers.


This conflict — juggling the realization that tech debt hinders the ability to deliver quality results faster to clients, but not wanting to prioritize and allocate the proper time necessary to clean it up, is what results in leadership pushing these sorts of “more unit test” campaigns. They view past redundant, slow, and disorganized spaghetti code baked into the software as a sort of sunk cost. They find it hard to understand why any time should be spent on rewriting it, and instead believe that everyone should just move on and switch their attention and energy into writing unit tests for future code commits. This view is compounded by the fact that implementing new features is what keeps customers engaged and interested in a product — customers don’t care about your tech debt or past development oversights, they just want new features built, and current features to be faster.


As such, it’s often tricky to get product managers and leadership onboard with the idea of dedicating an entire sprint (or more) to tackling tech debt when there is a backlog of customer requested features waiting to be implemented. That’s why the push for tech debt removal needs to be sold in a skillful way. Whenever possible, point out the fact that that removing the convoluted code in Module X would cut the development time for Feature Y in half, or make a report 200% times faster for a client to run. Hold routine meetings with PMs, management, and support staff reviewing any recent escalations and bugs. Show how an issue could have been mitigated faster if getting down to the root cause of the issue hadn’t involved hours of untangling spaghetti code. It’s easier to get buy-in when everyone realizes that messy code resulted in a customer not getting a fix in time and escalating a complaint up to the president of the company.


The Bottom Line


Unit tests should absolutely be included in code wherever possible to strengthen the quality of the codebase. However, they should be added strategically, and not just thrown around for the sake of adding more unit tests. Trying to gamify such metrics is pointless in general anyway, especially when there are so many additional actions that need to be taken to foster a stable system. Unit testing is not the sole magic solution to improving code integrity.


Moreover, adding new unit tests alongside new code does not retroactively fix existing tech debt and alleviate the overhead costs such debt poses to the maintainability of a system. If this tech debt isn’t proactively taken care of, someone will just have to clean it up to allow them to support unit tests eventually, so past tech debt is never avoidable for very long.


Refactoring efforts and initiatives to optimize current modules and remove existing debt should be prioritized. This will strip existing overhead from the code, and facilitate the process of writing higher quality unit tests in the future. A strong foundation of clean, organized, well structured code will lead the way for higher quality, and better unit tests down the line.

Tuesday, August 2, 2022

GACubeSolver version 1.0 release

I've released version 1.0 of GACubeSolver on my github page.

GACubeSolver is a Rubik's cube solver I wrote in Java. It uses the Swing framework for the UI. 


Instead of having hard-coded algorithms (like beginner's method, CFOP, Roux, etc) that the solver follows, the solver must figure out entirely on its own how to solve a scrambled cube. In order to achieve this, genetic algorithms are used. Traditionally speaking, this isn't the best use case for genetic algorithms due to the highly combinatorial nature of the Rubik's cube - but that's what makes this a fun and interesting challenge. 

So far, GACubeSolver can completely solve the pocket cube aka the 2x2 mini Rubik's cube (solve times vary depending on how scrambled the cube is). Getting the 3x3 fully solvable is the ultimate goal, and so far is a work in progress. 

The source code as well as a brief overview of the tech details and more details regarding the current support for the 3x3, can found on the project's github page.

Monday, August 16, 2021

260 Colors Generated with Cellular Automata



The block of code below produces the image shown above... can you figure out how?
(Continue reading on for the answer)




A couple weeks ago I wrote a post about using Rule 30, a type of cellular automaton, to build a PRNG (pseudo random number generator).

Since then, I've been playing around with the code used in that tutorial to see how much smaller I could make it. Turns out I was able to golf it a lot -- down to just 264 characters of javascript in fact (264 chars of JS + 70 of HTML).

This was the code before:

And this is the code now:

For the most part it was just about compressing the code, removing unnecessary html tags, and stripping as many semicolons as I could without breaking the code. I also changed the way I'm building the colors: you'll notice that instead of breaking the integer out into an RGB string, I'm now formatting them as hex codes so I could get back some of the chars I had spent on string concatenation and bitshifting.

I also ditched the arrow function after taking a closer look and realizing it wasn't even necessary. The whole reason I had this to begin with is that I need to know when I'm looking at cells 'outside' the bounds of a row in the Rule30 grid (the 2 leading and trailing blank cells that are always present on each row). 

So I golfed this line: b.push(f(i) ^ (f(i+1) || f(i+2)));

Down to: b.push(a[i]^(a[i+1]|a[i+2]));

Why are we able to do this? You might wonder, won't there now be cases when an index will be out of bounds and error out? Well actually no, because in JS attempting to fetch an element from an out of bounds index will just return undefined, which in this case is actually useful. When we use undefined as part of boolean logic, it will become false, (aka 0 when interpreted in math) anyway, which is what we want!

A couple other minor differences included notching up the main loop from 3120 to 6240 iterations so there are 260 colors instead of 130. And instead of a circle I let each color fan out off screen because I think it looks a lot cooler than the colorwheel.

A practical takeaway from this is getting to visualize Rule 30 from a different perspective. It's clear from looking at all the colors generated from such condensed code that Rule 30 is a truly interesting cellular automaton and there are likely a myriad of other use cases it can be applied to that have yet to be discovered.

If you happen to golf the code down even further, or want to share some other piece of code using Rule 30 in some unique way, go ahead and share in a comment below!

Tuesday, July 20, 2021

Generating Pseudo Random Numbers with Cellular Automata

Below is an image of a color wheel on an HTML5 canvas:




This is code that produces that image: 

Only 31 lines of JavaScript code, and yet the wheel is comprised of 130 unique colors. The same 130 unique colors each time too. Notice there's no loading data from external calls made to fetch data from other scripts or pages, no Math.random(), and no compressed data used. So how was this accomplished?


The answer to this puzzle is Rule 30, a cellular automaton introduced by Stephen Wolfram. As we'll see in a moment, the bits in the center column of Rule 30 are chaotic and complex and follow no easily discernible pattern. In this case, I collect those bits and concatenate them into integers that I use as RGB values in the wheel.


The rule set for Rule 30 is: Bitwise concatenate each group of 3 cells in a row. If the bitwise concatenation of the group is 1, 2, 3, or 4, then the resulting cell underneath will be 1 (represented by dark shaded cells).  Otherwise, 0, 5, 6, or 7 will be 0 (represented by light color cells).


Even simpler: for every 3 bit group, the resulting bottom cell will be the value of the left cell, XOR’d by the the result of the center cell OR’d by the right cell (Bottom Cell = Left Cell XOR (Center Cell OR Right Cell).


The GIF below illustrates how the first 4 rows of the Rule 30 automata are built when the initial input seed is 1:




Similar to other cellular automata, the rows are built repeatedly one after the other. The interesting thing about Rule 30 is that, unlike other automata, the rule set here produces a center column that exhibits complex and seemingly random behavior for many seed inputs (the input of 1 is an example of a seed that produces a complex behavior).


There are pretty much 2 ways you can exploit the center column to implement a pseudo random number generator. 


1.) At runtime populate Rule 30 data for n rows (i.e: 100). Then take some other runtime value that isn’t exactly random, but changes constantly and reliably — say the system current timestamp. Then mod this by the number of rows, and then start collecting the bits starting at that row in the center column for as many bits as you like (wrapping around if needed).


For instance, given a current timestamp of 1626103527, and 100 rows of generated Rule 30 data, and we want to build a 32 bit integer. We’d start at row 27 (1629103527 mod 100 = 27). Select bits from row 27 to 58 which yields the 32 bit number. 


2.) Take the timestamp and convert it to a string of bits (1626103527 in base 2 = 1100001000110100010010110100111) and then use that as the start seed. Only 32 rows would have to be generated to get a random number. Beware that a downside to this is there’s no guarantee the current seed will produce a random output. For instance, no combination of XOR and ORs will produce 1s given all nonzero inputs, so a seed of 0 will produce a ‘0’ in every row of the center. 


How you set up the data structures/arrays to accomplish either of the 2 methods is up to you.


In short: the first method requires generating more rows up front, but then we don’t need to generate any further rows after that. In the second scenario we plug in varying seed inputs each time and only need to generate the # of rows for the # of bits that we want our number to consist of, but it’s harder to guarantee that our dataset will be complex.


One more example with a visual:


We want to generate a random 8 bit value using the first method. We build 8 rows of the automata using an initial seed of 1. 



Then we bitwise concatenate those 8 bits together, which makes 11011100 in binary, or 220 in decimal.





Found this interesting and up for a challenge? Check out the contest that Stephen Wolfram has going offering $30,000 in total prizes to solve questions regarding the chaotic behaviors surrounding Rule 30 and why precisely it occurs.


Questions, comments, feedback, or have your own code snippets that uses Rule 30 or other cellular automata in unique or fun ways? Please share below in the comments!

Thursday, May 6, 2021

Genetic algorithms vs. Genetic Programming

In the Introduction to Genetic Algorithms video tutorial, we saw how GAs are powerful evolutionary algorithms that are used to synthesize solutions and optimize parameters for problems that are too complex or convoluted for humans to solve on their own. 


They work well in cases where semantics isn’t a huge concern. So in the case of the video, each possible step the mouse was able to take was encoded as an integer (gene). Strings of these genes (chromosomes) were built, and we kept evolving them through mutation and crossover until we found a chromosome that got the mouse from the start of the maze over to the cheese at the end. 


We weren’t concerned about what order the integers were in a chromosome, or if any of the states were “invalid”. Sure, there were some chromosomes that had a lot of noise or useless actions, but nothing that inherently didn’t make sense. This is what I mean by “semantics”.

So what happens if we wanted to use GA to evolve a mathematical expression? An area where semantics does matter.


The goal here will be to find a Mathematical expression that will yield the following outputs from inputs:


When input = 2, Output = 9

When Input = 0, Output = 5

When Input = 1, Output = 6

When Input = .5, Output = 5.125


The exact expression we’re looking for the computer to find is y = x^2 + 5.


We could encode each mathematical operation as an integer:


0: x

1: ^

2: *

3: +

4: -

5 through 9: (can be a number/digit in the range 0 to 5)


A few possible states we could generate:


701846 (would be 2x^3 - 1)

601846 (would be x^3 - 1)

031111 (would be x+^^^^)

222223 (would be *****+)

434343 (would be -+-+-+)


The first two states aren’t exactly the winning expression that we want, but they’re valid. The last 3 though are completely bogus. 


Even if we started with a proper string, a slight alteration during the mutation process can have a drastic result on the offspring. A generated expression such as 701846 undergoing a mutation could result in 101846, which now results in the expression ^x^3 - 1 — which doesn’t make much sense, does it? 


In order to deal with these invalid states, we'd need to include mechanisms into the parser that would repair or the damaged expressions prior to evaluating their fitness. It's certainly doable, but not very practical, and it’s definitely not scalable as we advance to building more complicated expressions. In theory, we’d need to manually code in more and more rules on how the computer should repair/handle invalid states. 



Enter Genetic Programming



Genetic programming has qualities that naturally safeguards it from spitting out faulty states like above.


Unlike GA, genetic programs are typically represented as a structure of “nodes” connected to each other somehow in memory. One of the more common structures used to represent them are trees.


The math function (4 + (X/25) - (9 * sqrt(X)) represented by a tree structure:


A math function represented by a tree structure



Since we’re dealing with a tree data structure now, we won’t be able to just mutate the genes by flipping a bit, or swapping the value of an integer - more effort will need to go in to the mutation and crossover operations (if you're looking for specifics on the code part, I'll be covering this part in detail in future articles).


But the nice thing here is that the restrictions are built directly into the tree, so we don’t need to deal with invalid states.


The root node, and also the internal nodes (nodes with child nodes) can only be operators. The terminal nodes (those without child nodes) can only contain constants or variables. We don’t connect an operator to a constant or variable node. Connecting nodes in this manner prevents us from generating a state like +X/-9+6.


It’s possible we could run into divide by zero errors, but these can be easily dealt with during the fitness evaluation stage of the GP. 


The bottom line


Practically speaking, there’s a fine line between what constitutes as a “genetic algorithm” or “genetic program”. At the end of the day — it really comes down to the classification and context of the problem you’re solving, the particular mechanics you’re using, and the format of the states that you’re generating.


In general, the primary differences come down to this:


Genetic Algorithms are traditionally built using strings of bits, integers, or characters that represent the genes that make up a chromosome. While this can make it pretty easy to manage and and evolve a GA, it can make evaluating them more cumbersome when they are applied to applications where they have a propensity to generate invalid states. Additionally, the fact that GA chromosomes are typically restricted by a fixed length results in decreased flexibility in the ability to scale as a problem becomes more complex.


With Genetic Programs, we adopt the same underlying evolutionary concepts and foundation from GA, but encode the individuals with a tree (or similar) structure rather than a string or list of genes. A consequence of this is that the overhead required to grow and maintain these structures is significantly increased. On the positive side, we have more control to enforce limits directly into the programs we build which greatly decreases the occurrence of invalid states. Due to the variable size of this type of representation, GP individuals are inherently more flexible than GA chromosomes, and can grow and scale as needed according to a problem's complexity — while efficiently preserving limits and rules throughout the tree.

Thursday, February 18, 2021

Checking if a string is a permutation of a Palindrome


During this post we’ll be writing an algorithm that determines whether a string is a permutation of a Palindrome. We’re not talking about finding out whether a string is a Palindrome, but whether if a string is a permutation of a Palindrome. 


In other words, given a string of scrambled characters, we want to see if it would be possible to rearrange those characters into a word that spells the forward as it does backward (i.e: “oonn” can be re-arranged to form the palindrome “noon”). 


You might be tempted to write code that finds every possible permutation of characters to see if one forms a Palindrome. This would work for small strings but would quickly become time consuming as the amount of possible permutations rises dramatically. And in fact, it’s also not needed. This is likely one such reason why this question (and variations of it) emerge frequently in interview questions and other coding brainteasers — to test the ability for you to take a step back — assess a problem and carefully break it down into its most critical components to produce an efficient and optimized solution.


“Wow”, “Racecar”, “Radar”, “Level”, “Noon” are all examples of palindromes. They each have the following qualities in common: all have an even count of characters, or at most 1 character with an odd # of occurrences. This means that instead of going crazy generating every permutation of a string and checking whether each is a palindrome, we just need to check if the string we start with abides by these rules. 

 

Let’s apply these rules to the string “aacecrr” and see if we can accurately conclude whether the letters in “aacecrr” can be rearranged into a palindrome. We’ll keep count of the # of times we’ve seen each character, along with a count that keeps track of the odd # of characters — incrementing by 1 when any current character count is odd, and decrementing it by 1 when any current character count is even. Below is an illustrated breakdown of what we’ll do as we iterate through each letter in the string (the current character and the counts being decremented/incremented are highlighted in blue):


A A C E C R R


A: 1  (1 is odd so numOdd should be incremented by 1)

numOdd: 1


A A C E C R R


A: 2  (2 is even so numOdd should be decremented by 1)

numOdd: 0


A A C E C R R


A: 2

C: 1 (odd so increment numOdd by 1)

numOdd: 1


A A C E C R R


A: 2

C: 1

E: 1 (odd so increment numOdd by 1)

numOdd: 2


A A C E C R R


A: 2

C: 2 (even so decrement numOdd by 1)

E: 1

numOdd: 1


A A C E C R R


A: 2

C: 2

E: 1

R: 1 (odd so increment numOdd by 1)

numOdd: 2


A A C E C R R


A: 2

C: 2

E: 1

R: 2 (even so decrement numOdd by 1)

numOdd: 1


By the end, numOdd is only 1, as it should be because we only have one unique character, “E”, that has an odd number of occurrences. And since we end with numOdd <= 1, isPermOfPalindrome will return true. You’ll notice with this logic we must scan every character of the string, as even though a character might have an even count at the beginning, it might eventually have an odd count at the end, and vice versa. We just won’t know until the end. 


Now we’re ready to translate this algorithm over to code. I’ve included my java implementation below:


I’m using a HashMap to keep track of the character counts, and just one integer, numOdd, to track the # of unique characters that have an odd frequency. 


Drop any comments or feedback below, and feel free to share your own implementation coded in another language if you have one!

Thursday, December 31, 2020

Google Foobar - Please Pass the Coded Messages

This week I got around to closing out Level 2 Part 2 of the Google Foobar Challenge - "Please Pass the Coded Messages"

Given a list of digits, our job is to find the order of those digits that yields the maximum number that is divisible by 3.


So a list like [3, 1, 4, 1, 5, 9] would yield 94311, and the list [3, 1, 4, 1] gives us 4311. 


Here’s something to keep in mind that will make our job easier: a number is divisible by 3 if and only if the sum of its digits is also divisible by 3. 


So we could have the following numbers: 375, 753, 357, and 735, and they’d all be divisible by 3, because the sum of their digits is divisible by 3.


If we were to rearrange the digits in the number ‘892’, they will never be divisible by 3, because the sum of their digits will always be 19, and 19 isn’t divisible by 3. We’re left with a non zero remainder each time:


298 / 3 = 99.33333333

829 / 3 = 276.3333333

289 / 3 = 96.33333333

892 / 3 = 297.3333333


This helps a lot. We just need to sort the list once in descending order: so [3, 1, 4, 1, 5, 9] becomes [9, 5, 4, 3, 1, 1], and then we just need to determine the digits to exclude from this sequence while still making it divisible by 3.


Let’s try with our first sequence of digits: [3, 1, 4, 1, 5, 9].


We’ll sort in descending order to yield: [9, 5, 4, 3, 1, 1]. Stringing these digits together we get 954311.


In this case, 954311 is not divisible by 3, so let’s try removing 4 from that series to get us the next largest number: 95311.


Well, 95311 is also not divisible by 3, so let’s take the 5 out and this time try with the same number of digits but keeping the 4 in there: 94311. 94311 is divisible by 3 (since 94311 / 3 = 31437) and so we stop. The answer is 94311.


The next list is easier: [3, 1, 4, 1]. After sorting we get 4311, and 4311 is divisible by 3 (4311 / 3 = 1437), so we end right there.


Pay attention to this tidbit in the instructions : “L will contain anywhere from 1 to 9 digits.”  We’re only going to be given short sequences to work with. Therefore, we can exploit bit masking to represent the combinations of digits that we’ll include/exclude from our number. 


We can start with a bit mask of 1 shifted left by the length of the list (6 in the case of [9, 5, 4, 3, 1, 1]) which yields the binary value 1000000 (or 64 in decimal). We then start counting backwards starting from mask - 1 (63 in decimal or 111111). For each bit position, 1 means we’ll include the digit from that position in the list, and 0 means we don’t include it:


111111 means we include all digits — which would be 954311. 


111110 would be every digit except the last one: 95431.


101010 would yield: 941.


If a combination is divisible by 3 and it’s the max number we’ve encountered, we’ll record it as the max value we have so far. We’ll return whatever that value is at the end. Sometimes it won’t be possible to create any number that is divisible by 3 from a sequence of digits, and in those cases we return 0.


Here’s my full answer in python, which passed all the test cases:


def solution(l):

    lLen = len(l)

    max = 1 << lLen

    l.sort(reverse=True)

    maxAttempt = 0

    for mask in reversed(range(max)):

        attempt = ""

        for index in range(mask.bit_length()):

            attempt += str(l[index]) if (mask >> index) & 1 == 1 else ''

        if attempt == '':

            attempt = 0

        attempt = int(attempt)

        if attempt % 3 == 0:

            if attempt > maxAttempt:

                maxAttempt = attempt


    return maxAttempt


And with that, it’s time to move on to level 3!

Thursday, December 17, 2020

Introduction to Genetic Algorithms

If you are interested in learning genetic algorithms and are looking for a quick introduction - watch this 10 minute tutorial I put together. It introduces you to the basics you'll need to start coding genetic algorithms from scratch. 

Share your questions/feedback/thoughts in the comments!











Monday, November 23, 2020

The 7 programming languages every coder should know

New programming languages seem to emerge constantly in the tech industry. How do you prioritize the ones to learn without spending the rest of your life bouncing from one new language to the next? Regardless of what languages you’re currently coding in, these 7 languages below are must learn for any coder hoping to stay competitive in the job market.


Python - Python is one of the most popular programming languages in the world. It’s straightforward, intuitive syntax, coupled with other aspects like its dynamic typing, contribute to its popularity among not just beginners, but all programmers in general. Its vast offering of libraries has fueled its expansion into a never-ending list of domains: data science, machine learning, statistics and analytics, web development, game making, robotics, are all areas python thrives in. Some of the most popular frameworks (i.e: PyTorch for machine learning, Django for web development, Pygame for making video games) are built on top of Python. Python can be used standalone, or used alongside other languages by extending them with python bindings [include link to bindings page]. Put simply, Python is everywhere. Although it’s certainly not the speediest language, its advantages and pervasiveness wins it a top spot on this list. 


Java - The stance on java seems to be polarized - there are those who love it, and those who avoid it at all costs. Regardless of what group you fall into, the fact is is that it’s in high demand and it’s not going anywhere anytime soon. If you’re looking into a company that is building large-scale enterprise class applications, it’s highly likely their tech stack is java-based. Get familiar with Java, practice building a RESTful service with Spring, and you’re already well on your way to landing a back-end development role. 


C# - A primary rival and competitor to Java, you’ll find this object-oriented language arise in a lot of the same use cases. If a company is producing enterprise applications and isn’t a java shop, there’s a very good chance C# is at the heart of its products. Created by Microsoft, C# was original officially supported by Windows only. Eventually it made its way over to linux and OSX via open source compilers like mono, and now is officially supported cross-platform by .NET core. Anything from little desktop apps to high powered web applications and APIs are written in C#. It will allow you to also break ground into some technologies that are predominantly java territory. Want to build mobile apps for Android, but don’t want anything to do with java? You could always use Xamarin with C# (although I personally wouldn’t recommend doing this based on experiences).


C - You might think its pointless to learn C unless you’re going to build an operating system or write firmware for embedded systems. Why bother with such an “old” technology? Although C is certainly a go-to in low level projects, you should still pick it up even if your plan is to stick with higher level languages. Coding in C will give you a newfound appreciation for the benefits that higher level and managed languages bring to the table. In languages like C# and Java, Garbage collection and memory management, for instance, are all handed in the background while you’re blissfully unaware of the work that is involved in those processes. As such, it’s easy for devs to take these features for granted. Just because you can safely ignore these processes doesn’t mean there aren’t opportunities to optimize your programs from a memory perspective, and coding in C will provide you with the skills to think in ways that will allow you to do exactly that.


Assembly (aka ASM) - When you’re making a function call in C — how are the arguments passed to the function? I don’t mean syntactically: spelling out the function name, followed by parentheses and the comma separated arguments, as is the common method in many languages, but I mean…. how are they actually passed? This is but one example of something so low level that you aren’t typically aware of it — even in C. Just like It’s hard to understand all the things that higher level and managed languages get you until you’ve programmed in C, it’s hard to realize the benefits you get with C until you’ve written in Assembly. Assembly is just about as low-level as you can get, second only to literally writing the machine code instructions yourself. Each architecture has its own assembly language. If you were going to assemble a program for Intel and AMD processors, you’d be writing either x86 or x64, (or both depending on if you’re assembling code for 32 or 64bit). Embedded devices (things like digital cameras, mobile phones, portable game systems) commonly run MIPS and ARM architectures, and so for those you’d be writing MIPS assembly and ARM assembly, respectively. All code, no matter how high level, will eventually result in the actual native machine code executing, so it makes a lot of sense to understand the language that boils down to machine code. 


Javascript - Virtually every modern web page you encounter will have some degree of javascript running in the background. Besides being the backbone of front-end frameworks like Rect, it can be used for back-end development as well via runtimes such as node.js, PurpleJS, and RingoJS. It’s also arguably the easiest of this list to get started with. You don’t need much to get started with it — you can start coding client-side javascript right now with the default text editors and browsers that ship with your OS. 


SQL - If you code professionally you’re going to come face to face with a SQL query at some point or another. Unless you’re planning on specifically becoming a SQL developer, you don’t need to master every minute detail and construct. However, every developer should be proficient in the basics: SELECT/INSERT/DELETE/CREATE TABLE statements, joins, views, etc. You should be fine with the basics when you’re a dev working in a codebase using and reading SQL in conjunction with the other primary project language(s).


By fostering a deep understanding of Python, C#, and Java, you’ve placed yourself in a high sought out position in the job market. Additionally, you’ll have a strong foundation of the OOP paradigm you can translate over to other object-oriented languages like C++ should you need or want to. Although learning lower level languages such as C and Assembly might not align with your immediate goals, they will give you a deeper understanding of the inner workings of software — instilling you with a mindset focused on efficiency and optimization. You’ll leverage this mindset to make wiser and more optimized design decisions, letting you stand out amongst peers who don’t have access to these experiences.