Sunday, March 29, 2015

Named Delegates Compared to Anonymous Delegates

I love to talk about lambda expressions because they are useful in a lot of situations (especially when using LINQ). Lambda expressions are just anonymous delegates. So the question comes up, What are the differences between named delegates and anonymous delegates?

For an overview of delegates in general, check out "Get Func<>-y: Delegates in .NET" (including a video series). For some more specifics on lambda expressions and anonymous delegates, take a look at "Learn to Love Lambdas (and LINQ, Too)".

There are 4 differences that we'll look at: captured variables, removing assignments, re-use, and visibility.

[Update 3/30: When I originally published this article, I left out re-use and visibility. Since these are so important, I amended this article rather than writing a new one. That's what I get for writing an article when I've had a long day.]

Captured Variables
Advantage: Anonymous Delegates
Anonymous delegates (and therefore lambda expressions) allow us to "capture" a variable that is currently in scope when we assign the delegate. Then we can later use that variable even when it would have normally gone out of scope.

Here's an example (from "Learn to Love Lambdas (and LINQ, Too)" -- check Page 8 of the PDF:


In this scenario, we have a method-level variable called "selectedPerson". This holds the currently selected item from a list box in the UI.

Then inside our lambda expression, we use that "selectedPerson" variable in order to locate a matching item in the list box after we load in fresh data.

The cool part of this is that the body of the lambda expression does not run until *after* the containing method (RefreshButton_Click) has completed. As such, the "selectedPerson" variable would normally go out of scope and be eligible for garbage collection.

But since our lambda expression captured this variable, it is not garbage collected, and it remains available for our anonymous delegate to make use of it. The big advantage to doing this is that we can have a method-level variable in this case. If we had a separate named delegate, then we would need to promote this to a class-level variable so that both methods have access to it. This allows us to scope our variables more appropriately.

Even though we use a lambda expression in our example, this works with other anonymous delegates as well. Other languages refer to this as a "closure". For a little more info on captured variables, check out this article: Lambda Expressions, Captured Variables, and For Loops: A Dangerous Combination.

So captured variables is a big advantage to using anonymous delegates.

Removing Assignments
Advantage: Named Delegates
All delegates are multicast delegates. This means that we can assign multiple methods to a single delegate variable. Then when we invoke the delegate (that is, execute the methods associated with the delegate), all of those methods run.

For an example of multicast delegates, take a look at "Get Func<>-y: Delegates in .NET" or check out the 3rd video in the series: Action<> & Multicast Delegates.

Just like we can add multiple methods, we can also remove those methods (at least when we're using a named delegate). Let's look at some code to see an example of this.

The Base Application
Here's a sample application where we can see the assignment differences:


In this application, we have buttons to add and remove named and anonymous delegates. The numbers will keep a running total of adding and removing. Then when we click the "Click Me!" button, the assigned delegates will execute.

You can get this code from GitHub: jeremybytes/delegates-and-func. This project is in the "BONUS-NamedVsAnonymous" folder of the solution.

The Constructor
Here are the basics of the code-behind for this form:


This is pretty straight-forward. We have 2 private fields to hold our running totals. Then in the constructor, we assign a lambda expression to our "Click" event to clear our output list in the UI.

As a reminder, event handlers are just special kinds of delegates. This means that they have all the properties that we expect with delegates -- including multicasting.

Named Delegates
Here's the code to handle our named delegate buttons:


First, notice our method "NamedDelegate". This method signature matches the event handler for a button: it takes an object and RoutedEventArgs as parameters and returns void. This means that we can assign this to our "Click" event.

The body of our method just outputs "Hello from Named Delegate" to our output list in the UI.

Then notice our "AddNamed_Click" method. This is hooked up to the event of the "Add" button. Here we see that we use "+=" to assign the "NamedDelegate" method as a handler for the event. The "+=" will add this delegate to any existing collection of methods that are already assigned.

The rest of this method manages the running total -- incrementing the count and updating the UI.

Now look at the "RemoveNamed_Click" method. This is hooked up to the "Remove" button. Here we see that we use "-=" to remove the assignment of the "NamedDelegate" method. The effect of this is that it will remove an assignment to this named delegate (assuming that it finds one by that name).

Anonymous Delegates
Now let's look at the code that is hooked up to the anonymous delegate buttons:


This code is similar to the "Add" and "Remove" methods that we have for the named delegates. The difference is that we are using anonymous delegates.

In the "Add" method, when we use "+=" we give it a lambda expression (which is an anonymous delegate). This will put "Hello from Anonymous Delegate" into our output list in the UI.

In the "Remove" method, we use "-=" to attempt to remove the anonymous delegate (just like we did with the named delegate). What we'll find is that this does not work quite like we intend it to.

Running the Application
So now that we have the code, let's run the application to see what happens. First we'll click each of the "Add" buttons 2 times, and then click the "Click Me!" button:


We can see that our running total tells us that we expect to have 2 named delegates and 2 anonymous delegates hooked up to our "Click" event. And we see exactly that in the output.

Now, let's click the "Remove" buttons one time each and see what happens:


This output isn't quite what we expect. Our running totals tell us that we expect to only have 1 named delegate and 1 anonymous delegate assigned. But our output shows that we have 1 named delegate and 2 anonymous delegates.
What this tells us is that the removal of the named delegate was successful, but the removal of the anonymous delegate was not.
Let's try again by clicking the "Remove" buttons again:


At least our results are consistent. The other named delegate was successfully removed, but we see that both anonymous delegates are still attached to our "Click" event.

Deconstructing the Behavior
This behavior makes sense when we take a closer look at what's happening. When we use the "-=" operator, we need to give an identifier that can be used to locate the item that we want to remove. When we have a named delegate, that's easy -- the name is "NamedDelegate", and it can remove the first one that it finds in the list.

But with the anonymous delegate, we don't have access to the name. The compiler generates a name, but we never see that in the code. Because of that, we can't give a valid value that will work with the "-=" operator in order to remove an anonymous delegate.

How Important Is This?
So that question is whether we should be concerned about this. The answer is: maybe. We can still clear out the items attached to the "Click" handler by just saying "xxx.Click = null". But this has the effect of clearing *everything*.

Why do we care whether things stay attached to event handlers? Well, depending on the circumstances, these are generally strong references. What that means is that event handler assignments can prevent the garbage collector from clearing objects from memory. Whether this is important really depends on how we do our assignments, the expected lifetime of our objects, and how we deal with these types of references.

As an alternative, we can use a different construct that will give us a weak reference. This is a reference that does *not* prevent the garbage collector from cleaning up an object. (But this is a topic for another day).

So we can see that being able to easily remove assignments is an advantage of named delegates.

[Update 3/30: When I originally published this article, I left out 2 very important differences: re-use and visibility. They're important enough that I added them on here rather than writing another article.]

Code Re-Use
Advantage: Named Delegates
A really big advantage to having named delegates is that we can reuse the same method throughout our application. If we use an anonymous delegate, we basically in-line the code. This is great for visibility (which we'll see in a bit), but it hampers the ability to reuse the same block of code somewhere else.

Here's an example from Get Func<>-y (also in the 2nd video in the series). In this sample, we have a delegate variable:


This represents a method that takes a "Person" as a parameter and returns a "string". And we assign it based on UI elements (in this case, a set of (badly-named) radio buttons).


This uses some static methods that we have in another class. Here's the code for "LastNameToUpper":


We can see that it matches our delegate signature (takes a "Person" as a parameter and returns a "string"), and it simply returns the last name property of the parameter as upper case.

The other methods look similar. And an advantage to having these separate methods (named delegates) is that we can re-use them in other parts of our application. This allows us to eliminate duplication -- which is good -- and adhere to the DRY principle (Don't Repeat Yourself).

So we can see that re-use is an advantage to using named delegates.

Visibility of Code
Advantage: Anonymous Delegates
There's one more difference that is important when we're talking about delegates, and that is visibility of the code. When we use an anonymous delegate, we basically in-line the code. This means that we don't need to go somewhere else to find the functionality.

Let's go back to the example that we just looked at:


If we want to know what any of these methods do, we need to click into them. For the most part, the method names tell us what they are doing, but we would need to click into the "FullName" method to know exactly what format comes out.

When we switch these to anonymous delegates (and in-line the code), all of the code is right in front of us:


Now we can see exactly what each delegate does. If we look at the last assignment now (which was previously assigned to the "FullName" method), we see that it formats the name as "LastName [comma] [space] FirstName".

The great thing about this is that we don't need to go somewhere else to find that. It's right here in front of us.

This is especially useful when we have lambda expressions in LINQ methods (which I really like). Now if our anonymous delegates get more than a few lines long, then they can become difficult to read and we may want to extract them as separate methods. But if they are short, it's great to have the code right in front of us.

So, visibility of the code is an advantage for anonymous delegates.

Wrap Up
There are pros and cons to pretty much every construct that we work with in programming. Delegates are no different.
Captured Variables: Advantage Anonymous Delegates
Removing Assignments: Advantage Named Delegates
Code Re-Use: Advantage Named Delegates
Visibility of Code: Advantage: Anonymous Delegates
As developers, we get to make these types of decisions all the time. It's important that we understand our tools so that we can make the best choices for our particular needs.

Happy Coding!

Looking for Answers: Origin of the Homicidal Developer

In my presentation "Clean Code: Homicidal Maniacs Read Code, Too!" (video & downloads), the homicidal maniac developer plays a key role (and leads to the best joke in the presentation):


I have this rule marked as "Unknown" because I'm not sure where it originated. I first heard the concept on a podcast a while back (sorry, I don't remember which one). I've been hoping to track down the originator of this because I would love to give him/her credit for it.

The Joy of the Internet
Shortly after I published my video last week, I saw my name come up in a Twitter thread that started here (just click on the photo to see the full thread):

Cesar took motivation from my Clean Code talk to include in his presentation on Agile Testing. This kicked off a discussion among Cesar and some other folks in Brazil about the origin of this concept (and thanks to Google Translate, I was able to follow the conversation -- the internet is pretty awesome).

Origins
That brings up the question of where this phrase originated. There's even a question that was posted on StackOverflow regarding this: Who wrote this programming saying?

Unfortunately, this was closed as being "not constructive". But through various sources (including my new friends in Brazil), I've found a couple of usages.

Usage: Damian Conway
Perl Best Practices, 2005
Damian Conway uses the following version in his book Perl Best Practices (on Page 5):
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
He does not give an attribution here (unfortunately). You can find this quote with a Google Books search.

Usage: John F. Woods
comp.lang.c++, 1991
John F. Woods uses the following version in a post in 1991:
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.  Code for readability.
From Google Groups

Variations
Another variation that I've seen (without attribution) is this one:
Always write code as though it will be maintained by a homicidal, axe-wielding maniac who knows where you live.
And, of course, there is the mutation that I use in my presentation:
Imagine the developer who comes after you is a homicidal maniac who knows where you live.
So there's lots of stuff out there that points to this concept.

Update 08/26/2015
Someone forwarded me this tweet. I haven't seen this one before:

A little bit of digging says this is from an unattributed sig that Martin Golding used in 1994. So similar to John Woods' sig above but a few years later.

Update 10/27/2015
Another attribution to Martin Golding. Can anyone confirm this? Martin?



OK Internet, Now It's Your Turn
I would love to give proper attribution to the concept of the axe-wielding, homicidal, violent, psychopath, maniac developer.

If you know the origins of this concept (or you have some references that point in the right direction), leave them in the comments. And if you'd like to claim credit yourself, that's fine with me. Just make sure that you provide some references that we can check out.

In the meantime, write code to keep the homicidal maniac happy.

Happy Coding!

Wednesday, March 25, 2015

Clean Code Presentation Recorded Live at Nebraska.Code()

I've been traveling quite a bit recently. Last week, I had the opportunity to go to Nebraska.Code() -- previously the Nebraska Code Camp now expanded into a larger event. A big thanks to Adam Barney and Ken Versaw for organizing a great event.

Clean Code Video
I made a recording of my presentation at the event: "Clean Code: Homicidal Maniacs Read Code, Too!" Watch the video on YouTube (or here): Clean Code



If you watch the presentation, you'll see that I have way too much fun doing this. I want to thank everyone who came because you are the people who give me this opportunity. I hope that you get some good information and are inspired to think about things is a bit of a different way.

Publishing The Watcher
As part of the presentation, I show a little helper application that encourages me to write code that is readable and maintainable. This application is called The Watcher, and every five minutes, it gives a pop-up as a reminder:


This is the homicidal maniac who takes over support for the code I write (and he knows where I live). I want to keep him happy. (BTW, there's a thread on Twitter talking about who originated the idea of the homicidal maniac developer. I'm trying to hunt down the origin so that I can give credit where it is due.)

Lots of folks have asked if they could have a copy of this application. I've been a bit embarrassed to publish it, though: the code is in pretty bad shape. I took an existing application (that showed a marquee on the screen) and modified it for the new functionality. In doing that, I didn't clean it up as I went (not a good example for a Clean Code presentation).

Well, I'm publishing it anyway. You can treat this as an exercise -- download the code and clean it up yourself to practice your new skills. And if you want to contribute back, feel free to make a pull request on the GitHub repo.

The code is available on GitHub, so check it out here: https://github.com/jeremybytes/the-watcher. (If you aren't a "git" person, just click the "Download ZIP" button on the right side of the screen.)

Meeting New People
I had a really awesome time at Nebraska.Code(). This is my first time speaking in the Midwest, so I got to meet a lot of new people, and spend some time with folks who I've only met in passing. I formed a lot of great relationships, and I'm looking forward to building on them in the future.

I can't emphasize enough how valuable talking to other developers has been for me. (And yes, I did meet new people in the lunch line at the event.) Just at this one event, I talked to people from Nebraska, Michigan, Missouri, Illinois, Kentucky, Iowa, Connecticut, Arizona, British Columbia, and several other places.

If you need some encouragement to talk to strangers at events, check out my article: Becoming a Social Developer: A Guide for Introverts.

I'll look forward to going to this event again next year, and I also look forward to speaking at a bunch of new events. It's a great opportunity to travel to new places and meet lots of new people.

Happy Coding!

Sunday, March 15, 2015

Lambda Expressions, Captured Variables, and For Loops: A Dangerous Combination

I get a lot of great questions and comments when I do live presentations. Last weekend at So Cal Code Camp was no different. Now it took me a full week to get to these because I've done 2 presentations and about 900 miles of driving this past week (and I'm still getting back into the swing of things). But better late than never.

Today we'll look at the dangers of capturing a variable from a "for" loop. This is something that I often mention in my presentations, but I've never actually put together a sample for this. At the So Cal Code Camp, I had someone come up and comment that she had run into this exact issue in her code. That's a great reason to sit down and put together a code sample to help others.

[Update 4/20/2015: A video version of this topic is also available: Captured Variables and for Loops.]

Captured Variables in Lambdas
In my presentation on Lambda expressions (Learn to Love Lambdas (and LINQ, Too!)), I show how captured variables work and what they're good for. If you're not familiar with captured variables, they are referred to as "closures" in other languages (leave it to Microsoft to pick their own terminology).

This means that we can we can "capture" a variable that is currently in scope when we assign our lambda expression (or anonymous delegate) and then use that variable later, even if it would have normally gone out of scope. For more information, you can check the PDF document or video with the session materials.

One of the great things about captured variables is that they allow us to scope things more appropriately. Instead of having a class-level variable that is accessible to everything in our class, we can have a method-level variable that is accessible to the original method and the anonymous delegate. (And this is exactly what the demo code from the session shows. So check out the code samples and walk-through.)

There's something very important to keep in mind about captured variables:
The value of a captured variable is the value at the time it is used, not the value at the time it was captured.
This means that if the value of the variable is changed after we capture it, we may get unexpected results.

Captured Variables in For Loops
Normally, we don't need to worry about this too much. But there is one situation where we may run into trouble: capturing the indexer of a "for" loop.

Let's run through some code to see the problem. And then we'll see that there is a very simple fix.

This code is available in the "BONUSCapturedVariables" branch of the "lambdas-and-linq" project on GitHub: https://github.com/jeremybytes/lambdas-and-linq. The code is in the "CapturedVariables" project of the solution.

We'll start with a class for our data. This is a "Person" object, and it's pretty simple:


This has 5 properties and an override of the "ToString" method. We'll be using this to output our objects to the console.

Capturing the Wrong Thing
In the console application, we'll start off with doing things the wrong way -- which also happens to be the logical way if we're new to captured variables:


In the "BadCapture" method, we have a "for" loop. Inside the loop, we create a new Task and use a lambda expression to call the "OutputPerson" method.

Notice the parameters of "OutputPerson": we have a list of person objects and an integer to represent the index of the item we want to output. (Granted, this is a bit of a contrived example, but it will show the problem that we run into.)

Now back in our lambda expression, we "capture" the indexer from the for loop (the "i" variable). So we would expect that for each new task (and each call to "OutputPerson") we would have a different value that would represent the current index of the "for" loop.

Let's see what happens. Here's the rest of the console application:


When we run the application, we get a runtime error:


This isn't good. If we look at this closely, we get an "ArgumentOutOfRange" exception. This probably means that we're trying to index past the end of our "people" collection.

Let's add a "try/catch" block to try to figure out what's going on:


Instead of getting a runtime exception, we'll get output to our console to show us the value of our parameter. And this is probably not what we expect:


This shows us that each time we call "OutputPerson", it is called with "7" as the index. Why? Because...
The value of a captured variable is the value at the time it is used, not the value at the time it was captured.
When the captured variables actually get used, the "for" loop has completed it's run. In this case, we have 7 items in our "people" collection. That means the final value for "i" (our indexer) is "7". So when the captured variable gets used, it's value is "7" (which happens to be beyond the end of our zero-based collection).

Fixing Things Up a Bit
Now before we look at the right way to do this, we're going to make a bit of a change to the "BadCapture" method. Since we're dealing with tasks, and we're not quite sure what order things will run/complete in, I want to make sure that all of the tasks from this method have completed before we move on. Here's the code for that:


We start by creating an array of Task objects. This will be the same as the number of items that we have in our collection (7).

Then inside the "for" loop, we save off the result of "Task.Run" (which happens to be a Task) as an element of our array.

Finally, by calling "Task.WaitAll(tasks)", we're specifying that this "BadCapture" method should not return until all of the tasks have completed. This will make sure that things don't get mixed up with our next set of tasks.

Capturing an Index the Right Way
The fix to this problem is pretty simple. Instead of capturing the indexer of the "for" loop, we create a local variable which holds a copy of the value. Here's that code in a separate method:


All we did here was create a variable that is local to the body of the "for" loop called "capturedIndex". This will be a variable that is a copy of the value of the "i" indexer. Since this is a value type (an integer), this makes a copy of the value.

Then notice that when we call "OutputPerson", we use the new "capturedIndex" as a parameter. This has the effect of capturing this new locally-scoped variable.

And on each iteration through the "for" loop, we get a new "capturedIndex" variable. So in the end, we capture 7 different variables. And each variable has the expected value because each separate instance of "capturedIndex" does not get changed after it is captured.

Here's our updated console application:


And when we run this, we get a much better output:


This shows us the original bad output plus the new good output. One thing to note is that our values are not coming out in any particular order. This is because the Tasks run when the task scheduler allows them to. If we run the application again, we get results in a different order:


This has to do with how Tasks work and is not affected by our lambda expressions or captured variables. We'll save this as a topic for another day. (In the meantime, feel free to take a look at my collected articles on Task, await, and asynchronous methods.)

Now as a final step, I'll add the same Task-handling to the "GoodCapture" method as we did to the "BadCapture" method. This will just make sure that all the tasks have finished running before moving on in the console application:


As a reminder, you can get all of this code in the "CapturedVariables" project of the lambdas-and-linq project on GitHub. The changes have all been rolled into the "master" branch, but you can see the specific branch with this code here BONUSCapturedVariables branch on lambdas-and-linq.

Wrap Up
Captured variables are really cool. They let us scope our variables more appropriately, and they can give us easy access to data without having to create a class-level or global variable. But we need to remember that the value of the captured variable is the value at the time we *use* it, not the value at the time we capture it.

We usually only run into problems when we try to capture variables that are constantly changing -- such as indexers in "for" loops. But as we've seen, there is a pretty simple solution to this. We just need to make a copy to a separate, unchanging local variable, and then capture that.

If you attend one of my presentations, please feel free to ask questions or make comments. These help me know what I should be writing about or expanding on. In my experience, if one person has a particular question, there are probably plenty of other folks who have it as well.

Happy Coding!

Thursday, March 5, 2015

Pluralsight Author Summit '15

I had the opportunity to go to the Pluralsight Author Summit in Salt Lake City, UT this past weekend. And it was an amazing experience. I wanted to let the experience settle a bit before I tried to write about it. The best parts of the event for me were really about the company, the authors, and the conversations.

The Company
Pluralsight as a company is dedicated to learning. This is not changing, and I don't see it changing in the future. The company has grown incredibly since I first got involved as an author (which was in May 2013). In less than 2 years, they have acquired several other companies, and the library has grown from around 400 courses to around 4000 courses.

With most companies, there would be a concern that a company cannot maintain its culture and core values with growth of that nature. But Pluralsight has managed to do this.

Here's a taste of that:


And this isn't corporate propaganda. I saw these values in the eyes of every employee that I talked to over the weekend. This is what they truly believe.

This goes all the way to the top. Two of the company founders stepped out of their "C" level roles because they were concerned that they would hold the company back. They stepped into roles that allowed them to continue to guide the company, but in different ways. That's the kind of dedication to values that I would like to see spread to other companies -- people focused on doing what's best to drive the vision forward and not just what's best for "me".

In addition, they talked about how each acquisition fits in to the overall goal of the company. This is not "buying out competitors", this is about finding companies with excellent content or tools that fit in well with furthering the Pluralsight mission. This is a strategic process focused on improving learning.

On a personal level, this makes Pluralsight a company that I want to be more involved with.

The Authors
Pluralsight has amazing authors. I don't know that I have ever been in such a large group where everyone is so passionate about what they are doing. I spent the weekend surrounded by people who knew their technologies and were dedicated to sharing that knowledge in the most effective way possible.

And everyone is incredibly open to sharing what they've learned about creating good courses. Some people were sharing tips on how to get the best audio recordings. Other people were talking about how to save time editing.

One author (Michael Perry) wowed everyone with a tool he created to make his own editing and recording process easier. He was soliciting input and has made the software available to other authors.

"Watch My Course"
Creating a course for Pluralsight is a lot of work. And a big reason for that is that the authors are fanatical about getting the best content with the best demos with the best audio with the best visuals all edited together in the best package. And this is why authors are always saying "watch my course". They aren't saying this because they are hoping to get more royalties; they say this because each and every one of them thinks that they have created something of high value (and they have). And they want to share that with as many people as possible.

And now's my chance to say "Watch my courses" ;-) http://www.pluralsight.com/author/jeremy-clark

The Conversations
I had direct conversations with close to 50 authors this weekend (yes, I was trying to keep track of who I met and talked to). There were tons of opportunities for casual and directed conversations, whether during sessions, at meals, at evening activities, and in the hallways. I met a lot of new people, and I got to catch up with folks I haven't seen for a while. I'm looking forward to continuing the conversations in the future.

Sometimes you meet people you already "know". I sat down and started talking to someone, and he told a joke about introvert developers. And I said, "Did you leave that on my blog?" (Comment on "Becoming a Social Developer: A Guide for Introverts"). He said, "Yes", and then we were instantly friends. (BTW, it was Steve Ognibene. Even though the comment has a fairly generic user name, it tracks back to both his blog and Twitter account.)

Open Space
There were 6 time slots dedicated to open space conversations. This is a format where the people pick topics that are important to them (or they think are important to others), they pitch the topic, and then people show up for great conversations. For more info, check Wikipedia for the Guiding Principles (and 1 Law).

The result was a lot of great sessions. Some were "Here's how I was successful with X"; others were "I have no idea how to do this, so let's get together and talk about ideas". Excellent discussions with people who have lots of good ideas. And there were folks from Pluralsight in each and every session, offering suggestions and thinking about ways Pluralsight can incorporate these great ideas.

Here's a photo of a session that I proposed and hosted:

I was simply the host for the discussion (I was at the front to write down ideas and try to make sure that everyone had a chance to speak). As Megan notes, it was a brilliant discussion. There were about 50 people in the room, and I'm amazed at how open everyone was -- willing to ask questions, sharing experience, and coming up with new ideas.

The conversations from this and other sessions continued through the weekend (and some of them are still going). These sessions were just the seeds to get things started.

Still More
I would be remiss not to talk about the formal content as well. Nancy Duarte did a keynote that was amazingly inspiring. You can get a taste of it from a TED talk that she did several years ago. Nancy shared how great presenters create something that resonates with the listeners/viewers. And this is something that we (as people who produce technical content) can bring to our own courses. Lots of great ideas.

The best part is that these are just small examples of the focus of the entire summit: how can we make things better for the learners.

I know this sounds like an ad for Pluralsight. But it's really hard not to sound like that. I've seen the dedication of the company itself; I've seen the values of the employees; I've seen the passion of the authors. After spending several days in this environment, it's inevitable that you want to be involved in it further.

Happy Learning!