0

Let's say, I would like to use a single object to represent a file and I'd like to get the filename (or path) of it so that I can use the name to remove the file or for other standard library procedures. I'd like to have a single abstraction which can be used with all available file-related standard library procedures.

I've found FileInfo but in my research I didn't find a get-file-name-procedure. File and FileHandle are pretty useless from a software engineering point of view because they provide no convenient abstraction and don't have members.

Is there a file abstraction (object) in Nim, which provides fast access to FileInfo as well as the file name so that a file doesn't need more than one procedure parameter?

ChrisoLosoph
  • 459
  • 4
  • 8

2 Answers2

5

There is no such abstraction in Nim, or any other language, simply because you are asking for an impossible thing to do with most filesystems. Consider the FileInfo structure and its linkCount field which tells you the number of hard links the file object has. But there is no way to get-a-filename from one or all of those links short of building and updating yourself a database of the whole filesystem.

While most filesystems allow access to files through paths, there is rarely a filesystem that gives paths from files because they actually don't need one! An example would be a Unix filesystem where one process opens a file through a path, then removes the path without closing the file. While the process holding the file open is alive, that file won't actually disappear, so you would have the case of a file without path.

The issue of handling paths, especially considering cross platform applications, involves its own can of worms: if you store paths as strings, what is the path separator and how do you escape it? Does your filesystem support volumes that require special case handling? What string encoding do paths use to satisfy all users? Just the encoding issue requires tons of tables and conversions which would bog down every other API wishing to get just a file like handle to read or write bytes.

A FileInfo is just a snapshot of the state of the file at a given time, a file handle is the live file object you can operate on, and a path (or many paths if your filesystem supports hard links) is just a convenience name for end users.

These are all very different things, which is why they are separate. Your app may need a more complex abstraction than other programmers are willing to tolerate, so create own abstraction which holds together all the individual pieces you need. For instance, consider the following structure:

import os

type
  AppFileInfo = object
    fileInfo: FileInfo
    file: File
    oneOfMany: string


proc changeFileExt(appFileInfo: AppFileInfo, ext: string): string =
  changeFileExt(appFileInfo.oneOfMany, ext)

proc readAll(appFileInfo: AppFileInfo): string =
  readAll(appFileInfo.file)

Those procs simply mimic the respective standard library APIs but use your more complex structure as inputs and transform it as needed. If you are worried about this abstraction not being optimised due to the extra proc call you could use a template instead.

If you follow this route, however, at some point you will have to ask yourself what is the lifetime of an AppFileInfo object: do you create it with a path? Do you create it from a file handle? Is it safe to access the file field in parts of your code or has it not been initialised properly? Do you return errors or throw exceptions when something goes wrong? Maybe when you start to ask yourself these questions you'll realise they are very app specific and are very difficult to generalise for every use case. Therefore such a complex object doesn't make much sense in the language standard library.

Grzegorz Adam Hankiewicz
  • 7,349
  • 1
  • 36
  • 78
  • Thank you. Saved me time for further search. I thought of a file object which is rather represented by it's path, not a file handle, living as long as the path lives without hard links. I also found `FileStream` which I find more comfortable than `File`. This is only because I wanted to remove a file and it requires a path to do so. That's why I was asking ;-) . – ChrisoLosoph Nov 30 '21 at 13:24
  • I was ambiguous: with lifetime of "path" I mean the time frame in which the path is needed by the process but removing the path from filesystem would remove the file as well so that the low-level concept of the file handle ID is abstracted away. Compared to other languages however, UFCS becomes handy here. It's still an ID but the set of non-virtual methods is theoretically unlimited. – ChrisoLosoph Nov 30 '21 at 13:43
2

I created the missing solution myself. I basically extended the File type using a global encapsulated table. Extending Types like this could be a useful idiom in Nim because of UFCS.

import tables

type FileObject = object
    file : File
    mode : FileMode
    path : string

proc initFileObject(name: string; mode: FileMode; bufsize = -1) : FileObject =
    result.file = open(name, mode, bufsize)
    result.path = name
    result.mode = mode

var g_fileObjects = initTable[File, FileObject]()
template get(this: File) : var FileObject = g_fileObjects[this]

proc openFile*(filepath: string; mode: FileMode = fmRead; bufsize = -1) : File =
    var fileObject = initFileObject(filepath, mode, bufsize)
    result = fileObject.file
    g_fileObjects[result] = fileObject

proc filePath*(this: File) : string {.raises: KeyError.} =
    return this.get.path

proc fileMode*(this: File) : FileMode {.raises: KeyError.} =
    return this.get.mode

from os import tryRemoveFile

proc closeOrDeleteFile[delete = false](this: File) : bool =
    result = g_fileObjects.hasKey(this)
    if result:
        when delete:
            result = this.filepath.tryRemoveFile()
        g_fileObjects.del(this)
        this.close()

proc closeFile*(this: File) : bool = this.closeOrDeleteFile[:false]
proc deleteFile*(this: File) : bool = this.closeOrDeleteFile[:true]

Now you can write


var f = openFile("myFile.txt", fmWrite)
var g = openFile("hello.txt", fmWrite)
echo f.filePath
echo f.deleteFile()
g.writeLine(g.filePath)
echo g.closeFile()
ChrisoLosoph
  • 459
  • 4
  • 8
  • If there is an easy way to write this table-idiom in Nim, like being able to define extra member fields for a type in a different scope then the original definition, please let me know. – ChrisoLosoph Nov 30 '21 at 21:17
  • I like your intriguing way of implementing `closeOrDeleteFile`. Where/how did you learn this technique and syntax? – hola Dec 05 '21 at 05:41