본문 바로가기
학교/CPL

Lecture 5: Functions, Substitution and Environments

by Hongwoo 2024. 3. 28.
반응형

목차

 

Learning Objectives

• Explain how function application works by substituting parameters for arguments
• Execute simple programs involving lambdas
• Explain what name shadowing and name capture is
• Implement recursion using only lambdas
• Explain how environments are delayed substitutions

 

 

Functions in Paret

Examples

 

 

Syntax (Lab/Extended)

 

 

Example

 

 

Example

 

 

 

Function Application

Parentheses around something is a function application (function call)

+, -, * are essentially built-in functions

→ 즉, let이나 lambda 같은게 괄호 안에 없으면 function application

 

 

 

Recursion

It gives an error because to call a function, you need to have already defined it

Free variable/identifier : It does not have a binding it can refer to

 

Recursion by Feeding Functions Themselves

When calling the function, I first call the function itself and then I pass my extra arguments so by wrapping it, I can refer to the location.

 

이것도 가능.

 

Currying

Currying: Turning a multi-argument function into a single argument function. 

The advantage of a curried function is that you can provide arguments one by one and construct partially applied functions, rather than having to provide them all at once.

Partial application: Providing a function with only some of its arguments, giving a new function for which only the remaining arguments must be provided.

 

 

Example in Paret

 

 

Why is currying useful?

Currying shows that single-argument lambdas are a simple core language for functions in general

Curried functions are convenient for when you want to partially apply a function with multiple arguments

 

 

What you want to do is create a new curried function where username and password are already set and just pass queries around. 

 

 

Three ways to Curry add in Scala

→ add is a function that takes an integer x and returns another function that takes an integer y and returns the sum of x and y. This function is curried.

→ This line applies the argument 1 to the add function, resulting in a new function incByOne that adds 1 to its argument.

→ In this line, incByOne is applied to 2, which results in 3. So, 3 is assigned to three.

→ In this line, you can also directly apply both arguments at once to the add function, resulting in the value of 3. This is possible because the function add is curried, allowing you to either apply arguments partially or all at once.

 

→ add is defined as a function that takes an integer x and returns another function that takes an integer and computes the sum of and y.

→ This line partially applies the argument 1 to the add method, resulting in a new function incByOne that adds 1 to its argument.

→ Here, incByOne is applied to 2, resulting in 3, so 3 is assigned to three.

→ Again, you can apply both arguments at once to the add method, resulting in the same value of 3.

 

 

→ A regular function add that takes two integer arguments x and y and returns their sum.

→ Here, add.curried(1) partially applies the first argument 1 to the add function. It returns a new function that takes the second argument y, and when called, it adds 1 to y. 

→ This line calls incByOne function with the argument 2, so 3 is assigned to three.

→ Here, you are directly calling the curried add function with both arguments 1 and 2, which results in 3.

 

 

Currying, more generally

 

 

 

Function Semantics

e [a / b] means: inside of expression e, substitute (replace) all instances of a with b

 

→ When you interpret a lambda, it has to become a value where we keep storing its parameter name and body without interpreting those because we don't know what's gonna come out of the function.

 

→ In the calling of a function, the first thing has to become a function value and the second thing is just interpreting the body, but we are going to substitute all parameters with the arguments. 

 

 

Properties 

Beta property: ( ( λ  ( x )  e1  )  e2  )  ↔  e1[x / e2]

→ Function application defined via substitution

 

Beta-eta property: ( ( λ  ( x )  e1  x )  e2  )  ↔  (e1  e2)

→ Function application works the same for all possible terms

→ No capturing of free variables 

 

 

Substitution

Substitution: Replacing a name with its corresponding name (expression).

A substitution-based interpreter would evaluate binding definitions (let or function applications) by directly replacing all occurrences of names referring to said definition in the sub-AST with their definition.

 

Example in Scala

 

 

Example in Scala

 

 

 

Example in Paret 1

 

 

 

 

Name Shadowing

Scala

 

silly(1)(2) → 4 (always refer to the closest variable)

 

Names always associate to binding that is lexically closest (i.e. closest in the program text)

Substitution should take this principle into account

 

 

Name Shadowing Java

Name Shadowing: when one binding hides another binding with the same name

 

Why shadowing matters: Why use the same variable twice? There are cases of using the same variable more than once e.g. in a library class

 

 

Name Shadowing Paret

You can always create a new value, even if the name was used before

→ Useful because Paret is immutable

→ Useful because everything in Paret is nested

→ Useful for e.g. libraries (overriding definitions)

 

 

Example

 

 

 

Name Capture

Name capture: When a free variable refers to the wrong binding (under static scoping), it is captured.

 

Free variables: variables that refers to nothing at its definition

Most languages disallow free variables

 

 

Capturing of Free Variables

 

Our free variable x was captured by the x binding coming from the let.

→ 여기서, 함수 선언할 때 x + y를 하는데, 이때 x가 선언되지 않은 상태임.

 

 

 

 

 

Avoiding Name Capture

 

 

 

Environments

Environments = delayed substitutions

Environments: A mapping from names to their corresponding meaning. By keeping track of binding definitions in an environment, the meaning of a name can be looked up when needed.

 

Binding

Bind = The following name will now represent this value, but only in a specific region, scope.

→ Variable, value, Function, Field, Method, class

 

 

Scope

Scope: The region where bindings are available

Example:

 

 

Static scoping (lexical scoping): The meaning of a name comes from the lexically closest definition. For functions, it means names refer to their closest definition at the definition site of the function.

 

Dynamic scoping: The meaning of a name is determined only by using the available names at the moment of evaluation (e.g. call site of functions). Few cases where languages use dynamic scoping, but it makes it easier to implement interpreters.

 

 

Environment

Environment: A way to store bindings

Idea: rather than directly substituting, always keep track of all names that are accessble and their corresponding values

We can look up values as needed

Advantage over substitution: Substitution has to pass over the AST to substitute, and has to substitute in parts that may never even be executed.

 

Example 1:

 

Example 2:

 

 

 

Making it simple in Paret

No mutation: values (constants) instead of variables

 

 

No forward Referencing: 

 

Why allow this?

- Difficult to implement and already disallowed in most cases, even in Java

 

Nested structure of Paret enforces clear lexical scoping

 

 

 

Environments Example (Java):

[ ] empty environment in the beginning

Line 1: [calculate →  ( )  => Int] 

Line 2: [calculate →  ( )  => Int, x → 1]

Line 3: [calculate →  ( )  => Int, x → 1, y → 2]

Line 4 - 7 (inside the if statement): [calculate →  ( )  => Int, x → 1, y → 2, z → 2], need to look up x and y in another environment 

Line 9: (여기에 z가 있으면 cannot lookup this value), 가장 최근 environment인 line 3에 있는 값들, 따라서 need to keep track of the environments

 

 

 

Environments Example (Paret):

f will be added to the green region, but f cannot reference to the green region (forward referencing)

 

 

 

Closures

Closes under substitution

 

 

Scoping Rules

Bodies of functions (lambdas) have access to their parameters

Bodies of functions (lambdas) have access to bindings that are available when they are defined

Bodies of functions (lambdas) do not have access to bindings that are available when they are executed → This would be capturing

 

 

 

When we define a function, let's store what it had access to at that point. So rather than storing the function as a value with its  (parameter, body), we are also going to store vwhatever it had access to when it got defined → Environment as it was when it was defined.

 

 

Why we need closures

 

y is free

f doesn't have access to anything (empty environment)

f → Closure

 

 

 

 

Semantics (Environments)

 

 

 

TEXTBOOK NOTES

Function Application:

FdC(params: List[String], body: ExprC)
AppC(f : ExprC, args: List[ExprC])

 

여기서 params는 함수의 parameter들. Body는 함수에서 해야하는거.

 

Function Application: 

1. Look up the function definition (f)

2. Evaluate its body

반응형

'학교 > CPL' 카테고리의 다른 글

CPL 3: Function Interpretation and Environments  (0) 2024.03.31
Lecture 6: Environments and Scoping  (0) 2024.03.29
CPL 2-3: Higher Order Functions  (1) 2024.03.27
Lecture 4: Functions  (1) 2024.03.27
CPL 1-2: Semantics and Transformations  (0) 2024.03.26

댓글