public class LambdaTest { public void example() { char result1 = convertChar('h', otherMethod); char result2 = convertChar('h', (int i, string s) => { return false; } ); Func<int, string, bool> testLambda = (int i, string s) => { return false; }; char result3 = convertChar('h', testLambda); } private char convertChar(char ch, Func<int, string, bool> func) { // Nonsense function to demonstrate function as parameter if (func(1, "test string")) { return ch; } else if(func(2, "abc")) { return 'a'; } return 'z'; } private bool otherMethod(int i, string s) { // Do something here return true; } }
A tenant of good code design is "single point of control". A simple way to understand this is "No repetition of code". Not exactly, but for now it's a good enough definition.
A lambda function, sometimes called an “anonymous” function is a nameless helper function, too small or too specific to give a name. Functions, in C# are First Class Citizens, meaning they have the same “rights” as other structures like variables. Most notably, first class citizens can be passed to a function as an argument or parameter.
In the following examples, we will explore the use of functions as parameters as a way to simplify and make our code more elegant.
Consider names of background characters in video games. Characters who the game designer wants to name but doesn’t really care about what the name is. One way to generate names is by taking existing names and changing a few letters. The results of this algorithm are suitably random/ exotic but also recognizable enough so as not to totally alienate the player. In an effort to practice something new, lets totally over engineer this problem and use lambda functions.
public static generateName() { // A function that generates a name return simpleScrambleAlg("ender".ToCharArray()); } private static string simpleScrambleAlg(char[] oldName) { // NOTE: Will always return a new string, and never modify oldName arg // 1) pick rand char to convert keeping v/c type // 2) pick rand v to convert to v // 3) pick rand c to convert to c }
This is the basic outline of what we want: A function that returns a string with no given input. In an effort to build a small interface, let's put the actual algorithm in its own function.
Let's impliment step 1) pick rand char to convert keeping v/c type. In simple terms: Pick a random letter from oldName and change it, but vowels stay vowels and consonants stay consonants
private static string simpleScrambleAlg(char[] oldName) { // NOTE: Will always return a new string, and never modify oldName arg // 1) pick rand char to convert keeping v/c type // 2) pick rand v to convert to v // 3) pick rand c to convert to c char[] safeOldName = (char[])oldName.Clone(); int randIndex = UnityEngine.Random.Range(0, safeOldName.Length); char randChar = safeOldName[randIndex]; char replacement; if (isVowel(randChar)) { // pull a random new vowel replacement = // TODO } else { // pull a random new consonant replacement = // TODO } safeOldName[randIndex] = replacement; // TODO: steps 2 and 3 of the above algorithim }
Explination (skip this if you're comfortable with the code above):
Clone oldName so we can do sugery/ modifications without messing up the origional.
Use char[] (char array) because we want to operate on individual characters, and using an array is easier than
using a string.
Then pick a random number between 0 and the size of the char[]. This will be our index
Pull out the character at the randIndex, and depending on if it's a
vowel or not we generate a different kind of replacement char.
Then, take the replacement char and put it back into the char[] at the same index.
Now we fix the first TODO, generating a random vowel
private static char getRandVowel() { int randIndex = UnityEngine.Random.Range(0, NameGen.vowels.Count); return NameGen.vowels[randIndex]; // A listed enumeration of all vowels }
Look at the bottom for the full code which includes the list NameGen.vowels
Generating a random vowel is well and good, but we want a guarantee that the new vowel is different from the old vowel. We need another method to interpret the results of getRandVowel().
private static char convertVowel(char oldChar) { /** * Provided an old character this method will generate a random new * vowel (guaranteed different from oldChar) * * If oldChar is empty string, a random new vowel will be returned */ char newChar = getRandVowel(); while (oldChar == newChar) { newChar = getRandVowel(); } return newChar; }
Is convertVowel() guaranteed to return? No.
Do I care? No.
While
it is possible that getRandVowel() returns the same vowel every time,
and therefor never breaks the while loop, this is incredably unlikely
and the player and designer will have bigger problems if that ever
happens.
Let's add this back to our simpleScrambleAlg() function.
private static string simpleScrambleAlg(char[] oldName) { // NOTE: Will always return a new string, and never modify oldName arg // 1) pick rand char to convert keeping v/c type // 2) pick rand v to convert to v // 3) pick rand c to convert to c char[] safeOldName = (char[])oldName.Clone(); int randIndex = UnityEngine.Random.Range(0, safeOldName.Length); char randChar = safeOldName[randIndex]; char replacement; if (isVowel(randChar)) { // pull a random new vowel replacement = convertVowel(); } else { // pull a random new consonant // TODO } safeOldName[randIndex] = replacement; // TODO: steps 2 and 3 of the above algorithim }
Now do the same for convertConsonant().
private static string simpleScrambleAlg(char[] oldName) { // NOTE: Will always return a new string, and never modify oldName arg // 1) pick rand char to convert keeping v/c type // 2) pick rand v to convert to v // 3) pick rand c to convert to c char[] safeOldName = (char[])oldName.Clone(); int randIndex = UnityEngine.Random.Range(0, safeOldName.Length); char randChar = safeOldName[randIndex]; char replacement; if (isVowel(randChar)) { // pull a random new vowel replacement = convertVowel(); } else { // pull a random new consonant replacement = convertconsonant(); } safeOldName[randIndex] = replacement; // TODO: steps 2 and 3 of the above algorithim } ... private static char convertConsonant(char oldChar) { /** * Provided an old character this method will generate a random new * consonant (guaranteed different from oldChar) * * If oldChar is empty string, a random new consonant will be returned */ char newChar = getRandConsonant(); while (oldChar == newChar) { newChar = getRandconsonant(); } return newChar; } private static char getRandconsonant() { int randIndex = UnityEngine.Random.Range(0, NameGen.consonants.Count); return NameGen.consonants[randIndex]; // A listed enumeration of all consonants }
Cool, now we have step 1 completed in our simpleScrambleAlg(). But, something's wrong. Not functunally but with our design. Look at convertConsonant() and convertVowel() side by side. These two functions are almost identical, with only minor differences between them.
private static char convertConsonant(char oldChar) { /** * Provided an old character this method will generate a random new * consonant (guaranteed different from oldChar) * * If oldChar is empty string, a random new consonant will be returned */ char newChar = getRandConsonant(); // different while (oldChar == newChar) { newChar = getRandConsonant(); // different } return newChar; } private static char convertVowel(char oldChar) { /** * Provided an old character this method will generate a random new * vowel (guaranteed different from oldChar) * * If oldChar is empty string, a random new vowel will be returned */ char newChar = getRandVowel(); // different while (oldChar == newChar) { newChar = getRandVowel(); // different } return newChar; }
It'd be super cool if we can find a way to merge these pieces of code. Some way to abstract the character generator functions. Well, because C# considers functions first class citizens we can pass it in as a paramater!
private static char convertChar(char oldChar, Func<char> charBuilder) { /** * Provided an old character and a function that randomly generates characters, * this method will generate a random new character (guaranteed different from oldChar) * from the outputs of charBuilder arg. * * If oldChar is empty string, a random new char will be returned from the charBuilder's pool */ char newChar = charBuilder(); while (oldChar == newChar) { newChar = charBuilder(); } return newChar; }
This signiture changes breaks our simpleScrambleAlg(). We no longer have convertVowel() or convertConsonant(), only convertChar(). To keep the functunality the same as before, we need to pass in the character generator functions getRandVowel and getRandConsonat.
The way to conseptualize this code is by noticing the division of
behavior. The getRandVowel/ getRandConsonat functions are the engine.
They're just dumb generators that build a specific object. The
convertChar function is the logic that sits ontop of an engine and
processes the output in a certain way. We want this process to be the
same regardless of what characters are being generated.
This allows
for easily swapping out new sets of characters in the generation pool.
If for instance we decided we don't like names with the letter 'z' we
can build a getRandConsonatNoZ() function and it'll still work in the
convertChar() function.
private static string simpleScrambleAlg(char[] oldName) { // NOTE: Will always return a new string, and never modify oldName arg // 1) pick rand char to convert keeping v/c type // 2) pick rand v to convert to v // 3) pick rand c to convert to c char[] safeOldName = (char[])oldName.Clone(); int randIndex = UnityEngine.Random.Range(0, safeOldName.Length); char randChar = safeOldName[randIndex]; char replacement; if (isVowel(randChar)) { replacement = convertChar(randChar, getRandVowel); } else { replacement = convertChar(randChar, getRandConsonat); } safeOldName[randIndex] = replacement; }
Now, this is where I stopped with my design. I think having a stand alone method for each letter generator is good design and makes sense to me. However, for the sake of a tutorial, let's talk about lambda functions.
A lambda function is a function that takes input and output but is un-named. It has a funky looking syntax, but I'll break down each part.
FunctestLambda = (int i, string s) => { if (i > 10) { return "hello"; } else { return s; } };
I'm declaring a function variable with the name testLambda (ironic I know, naming the nameless). The first set of parenthesis (...) defines the input variables to the lambda. This syntax looks like the standard function input syntax for regular functions. The '=>' sign signals the start of the lambda and is called the '=> operator'. Everythign inside the curly braces {...} is a regular function, althought it's not formatted well.
Using a lambda, we can rewrite our code to look like this.
private static string simpleScrambleAlg(char[] oldName) { // NOTE: Will always return a new string, and never modify oldName arg // 1) pick rand char to convert keeping v/c type // 2) pick rand v to convert to v // 3) pick rand c to convert to c char[] safeOldName = (char[])oldName.Clone(); int randIndex = UnityEngine.Random.Range(0, safeOldName.Length); char randChar = safeOldName[randIndex]; char replacement; if (isVowel(randChar)) { // pull a random new vowel replacement = convertChar(randChar, () => { int ri = UnityEngine.Random.Range(0, NameGen.vowels.Count); return NameGen.vowels[ri]; }); } else { // pull a random new consonant replacement = convertChar(randChar, () => { int ri = UnityEngine.Random.Range(0, NameGen.consonants.Count); return NameGen.consonants[ri]; }); } safeOldName[randIndex] = replacement; // TODO: steps 2 and 3 of the above algorithim }
While I think this looks messy, other developers would perfer it. Looking at this code again, I can now clearly see a place to reduce my code some more. The two generator functions are almost identical, perhapse we can even do away with the entire notion of a function and simply pass in a list of letters (which would defete the pourpose of learning lambdas but oh-well)! However, I'll be keeping my code the way it is below. Passing in a function to generate a vowel is more powerfull than a list. With a function, I can easially weight the vowel list favoring the letter A over the letter O for instance.
public class NameGen : MonoBehaviour { public void Start() { // string newName = simpleScrambleAlg("ender".ToCharArray()); for (int i = 0; i < 300; i++) { string newName = SylableNameGen.genSylableName(); //Debug.Log(newName); lc.addEvent(newName); } } public LogController lc; public void genNameTest() { string newName = simpleScrambleAlg("testabcde".ToCharArray()); lc.addEvent(newName); } private static string simpleScrambleAlg(char[] oldName) { // NOTE: Will always return a new string, and never modify oldName arg // 1) pick rand char to convert keeping v/c type // 2) pick rand v to convert to v // 3) pick rand c to convert to c char[] safeOldName = (char[])oldName.Clone(); int randIndex = UnityEngine.Random.Range(0, safeOldName.Length); char randChar = safeOldName[randIndex]; char replacement; if (isVowel(randChar)) { replacement = convertChar(randChar, getRandVowel); } else { replacement = convertChar(randChar, getRandConsonat); } safeOldName[randIndex] = replacement; // 2) // Find all the vowels in the oldName ListvowelIndex = new List (); List consonantIndex = new List (); for (int i = 0; i < safeOldName.Length; i++) { if (isVowel(safeOldName[i])) { vowelIndex.Add(i); } else { consonantIndex.Add(i); } } // Pull a random index from the vowel list int randomVowelIndex = vowelIndex[UnityEngine.Random.Range(0, vowelIndex.Count)]; char randomVowel = safeOldName[randomVowelIndex]; replacement = convertChar(randomVowel, getRandVowel); safeOldName[randomVowelIndex] = replacement; // 3) int randomConsonantIndex = consonantIndex[UnityEngine.Random.Range(0, consonantIndex.Count)]; char randomCons = safeOldName[randomConsonantIndex]; replacement = convertChar(randomCons, getRandConsonat); safeOldName[randomConsonantIndex] = replacement; return new string(safeOldName); } public static char convertChar(char oldChar, Func charBuilder) { /** * Provided an old character and a function that randomly generates characters, * this method will generate a random new character (guaranteed different from oldChar) * from the outputs of charBuilder arg. * * If oldChar is empty string, a random new char will be returned from the charBuilder's pool */ char newChar = charBuilder(); while(oldChar == newChar) { newChar = charBuilder(); } return newChar; } public static bool isVowel(char c) { return vowels.Contains(c); } public static bool isConsonant(char c) { return !isVowel(c); } public static char getRandVowel() { int randIndex = UnityEngine.Random.Range(0, NameGen.vowels.Count); return NameGen.vowels[randIndex]; } public static char getRandConsonat() { int randIndex = UnityEngine.Random.Range(0, NameGen.consonants.Count); return NameGen.consonants[randIndex]; } public static char getRandChar() { List allLetters = new List (vowels); allLetters.AddRange(consonants); int randIndex = UnityEngine.Random.Range(0, allLetters.Count); return allLetters[randIndex]; } public static readonly List vowels = new List () { 'a', 'e', 'i', 'o', 'u' }; public static readonly List consonants = new List () { 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' }; }