How to properly document code?

asudox@lemmy.world to Programming@programming.dev – 24 points –

I just recently started documenting my code as it helped me. Though I feel like my documentations are a bit too verbose and probably unneeded on obvious parts of my code.

So I started commenting above a few lines of code and explain it in a short sentence what I do or why I do that, then leave a space under it for the next line so it is easier to read.

What do you think about this?

Edit: real code example from one of my projects:

async def discord_login_callback(request: HttpRequest) -> HttpResponseRedirect:
    async def exchange_oauth2_code(code: str) -> str | None:
        data = {
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': OAUTH2_REDIRECT_URI
        }
        headers = {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
        async with httpx.AsyncClient() as client:
            # get user's access and refresh tokens
            response = await client.post(f"{BASE_API_URI}/oauth2/token", data=data, headers=headers, auth=(CLIENT_ID, CLIENT_SECRET))
            if response.status_code == 200:
                access_token, refresh_token = response.json()["access_token"], response.json()["refresh_token"]

                # get user data via discord's api
                user_data = await client.get(f"{BASE_API_URI}/users/@me", headers={"Authorization": f"Bearer {access_token}"})
                user_data = user_data.json()
                user_data.update({"access_token": access_token, "refresh_token": refresh_token}) # add tokens to user_data

                return user_data, None
            else:
                # if any error occurs, return error context
                context = generate_error_dictionary("An error occurred while trying to get user's access and refresh tokens", f"Response Status: {response.status_code}\nError: {response.content}")
                return None, context

    code = request.GET.get("code")
    user, context = await exchange_oauth2_code(code)

    # login if user's discord user data is returned
    if user:
        discord_user = await aauthenticate(request, user=user)
        await alogin(request, user=discord_user, backend="index.auth.DiscordAuthenticationBackend")
        return redirect("index")
    else:
        return render(request, "index/errorPage.html", context)
30

You are viewing a single comment

For new code I'm writing I'm using mostly JsDoc function headers on public methods of classes and exported functions. With one or two sentences explaining what function does.

Also try to explain what to expect in edge cases, like when you pass am empty string, null, ... stuff of that nature - for which I then create unit tests.

I also always mention if a function is pure or not or if a method changes the state of its object. On a sidenote I find it odd that almost no language has a keyword for pure functions or readonly methods.

If I add a big new chunk of code that spans multiple files but is somewhat closed off, I create a md file explaining the big picture. For example I recently added my own closed off library to my angular frontend that handles websocket stuff like subscribing, unsubscribing, buffering, pausing,... for which a created a md file explaining it.

What is a pure function? Never heard that before.

Essentially a function that doesn't produce side effects, like modifying variables outside of its scope or modifying the function parameters. This something you should always try to incorporate into your code as it makes it much easier to test and makes the function's use less risky since you don't relay on external unrelated values.

To give you an example in JavaScript, here are two ways to replace certain numbers from an other list of numbers with the number 0

first a way to do it with a non pure function :

let bannedNumbers = [4,6]

const nums = [0,1,2,3,4,5,6,7,8,9]

function replaceWithZero(nums){
    for (let i = 0 ; i < nums.length; i++){
        if (bannedNumbers.includes(nums[i])){
            nums[i] = 0
        }
    }
}
replaceWithZero(nums)
console.log("numbers are : ", nums)

here the function replaceWithZero does two things that make it impure. First it modifies its parameter. This can lead to issues, for example if you have Second it uses a non-constant variable outside of its scope (bannedNumbers). Which is bad because if somewhere else in the code someone changes bannedNumbers the behavior of the function changes.

A proper pure implementation could look something like this :

const nums = [0,1,2,3,4,5,6,7,8,9]
function repalceWithZero(nums){
    const  bannedNumbers = [4,6]
    const result = []
    for(const num of nums){
        result.push(bannedNumbers.includes(num) ? 0 : num)
    }
    return result
}
const replaced = replaceWithZero(nums)
console.log("numbers are : ", replaced)

Here we are not modifying anything outside of the function's scope or its parameters. This means that no matter where, when and how often we call this function it will always behave the same when given the same inputs! This is the whole goal of pure functions.

Obviously in practice can't make everything 100% pure, for example when making a HTTP request you are always dependent on external factors. But you can try to minimize external factors by making the HTTP request, and the running the result only through pure functions.

I really wouldn't call anything that hits the network pure, because errors are quite likely. But I guess we all put the bar at a different level, I would not count logging as a side effect yet I've been bitten by overly verbose logs in hot loops.

const-ness gives a mini version of purity, although nothing prevents someone from opening /etc/lol in a const function... I think GCC has a pure attribute but I don't think it's enforced by the compiler, only used for optimizations