Others have mentioned closures; there are also a few other patterns that can do similar things.
One is "ad-hoc polymorphism", which lets us associate values with types. This is essentially the same as implementing an interface in languages like Java, except we don't need to modify any existing definitions (e.g. we don't need to edit a class to say "extends Foo"). This is useful for picking between pre-defined sets of values, e.g. for your example of logger and base dir we can define an interface like 'Environment' with methods returning a base dir and a logger; then we can define a 'Production' type which implements this interface by providing the real logger and base dir, a 'Test' type which returns a temp dir and stdout logger, a 'Dev' type whose logger keeps verbose debug messages, etc. We can write our code in terms of a generic Environment, then in our 'main' function we specify that we're using 'Production'; in our test suite we specify that it's 'Test'; when debugging we can specify 'Dev'.
Another approach is called 'Reader', and it's based around the following type and helper functions (in Haskell syntax):
data Reader a b = R (a -> b)
read :: Reader a a
read = R (\x -> x)
runReader :: Reader a b -> a -> b
runReader (R f) x = f x
Or in Scala:
final class Reader[A, B](f: A => B) {
def run(x: A): B = f(x)
}
final object Reader {
def read[A]: Reader[A, A] = new Reader(x => x)
}
Note that this type literally just contains functions from some type A to another type B. The function 'read' is a wrapped-up identity function (returning its argument unchanged), the 'run' function just applies a wrapped-up function to an argument.
This gets interesting if we do two things. First we can specialise the input type (a or A), e.g. in Haskell:
type Application t = Reader (Path, Logger)
Or in Scala:
type Application[T] = Reader[(Path, Logger), T]
The type 'Application[T]' represents functions from a Path+Logger pair to a T.
Next we can define some functions for manipulating Reader values:
-- Applies a function f to a Reader's result; AKA function composition
map :: (a -> b) -> Reader t a -> Reader t b
map f (R r) = R (\x -> f (r x))
-- Wrap up a value into a Reader, by accepting an argument and ignoring it
wrap :: a -> Reader t a
wrap x = R (\y -> x)
-- Combine two Readers, so they're given the same argument and their return values are paired
product :: Reader t a -> Reader t b -> Reader t (a, b)
product (R f) (R g) = R (\x -> (f x, g x))
-- "Unwrap" nested Readers, by passing the argument into the result
join :: Reader t (Reader t a) -> Reader t a
join (R f) = R (\x -> runReader (f x) x)
Together these functions let us write arbitrary functions "inside" this 'Application' type. For example:
-- The 'fst' function gets the first element of a pair
baseDir :: Application Path
baseDir = map fst read
-- The 'snd' function gets the second element of a pair
logger :: Application Logger
logger :: map snd read
-- Use the logger to log the given string (via some 'logWith' function)
log :: String -> Application ()
log msg = map (\l -> logWith l msg) logger
-- Read the contents of a given file from the base dir
openFile :: Filename -> Application String
openFile f = map (\d -> readContents (d + "/" + f)) baseDir
-- Log the contents of a given file from the base dir
logContents :: Filename -> Application ()
logContents f = join (map log (openFile f))
Note how we can only access the base dir and logger from 'inside' the Application type; i.e. we can get an 'Application Logger', but we can never just get a 'Logger'. That's because the code doesn't actually define a logger or base dir anywhere; any value 'Application T' is really just a function '(Logger, Path) -> T'. To extract a result, we need to give it to 'runReader', which (in the case of Application) also needs to be given a '(Logger, Path)' pair.
Using these functions directly can be a bit tedious. Some FP languages have special syntax which makes it nicer, e.g. in Haskell we can write things like:
do x <- foo
y <- bar x
baz x y
The Scala equivalent is:
for {
x <- foo
y <- bar(x)
z <- baz(x, y)
} yield z
These get rewritten to (the equivalent of):
join (join ((map (\x -> map (baz x) (bar x)) foo))
This API is a combination of Functor (map), Applicative (wrap and product) and Monad (join). Hence people sometimes call this "the Reader monad"; but don't let that scare you!